@feiyang666/dsh-usage-plugin 1.14.1 → 1.16.5

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,44 @@
8
8
 
9
9
  ---
10
10
 
11
+ ## v1.16.5 (2026-08-29)
12
+
13
+ ### 新增 / New
14
+
15
+ - **概览页 Hero 主指标卡**:顶部新增「本月已消耗」渐变大数字卡 + 当前计费时段徽章;设置月度预算后同卡显示预算使用进度条(≥80% 黄色提醒,≥100% 红色超支告警并显示超支金额)。
16
+ - **核心指标卡片分层**:原 7 张等大统计卡改为「3 大主卡(总消耗 / 调用次数 / 缓存命中率)+ 4 小辅助卡(输入·未命中 / 输出 / 高峰消耗 / 空闲消耗)」,关键指标视觉权重突出。
17
+ - **近 30 天消耗趋势图**(canvas 堆叠柱状):概览页新增按日消耗趋势,高峰(橙)/ 空闲(蓝)堆叠着色,悬停显示当日高峰/空闲/总消耗与调用数。
18
+ - **模型消耗占比环形图**:概览页新增各模型消耗占比环形图 + 图例(模型名 + 百分比)。
19
+ - **会话消耗排行 Top 10**:概览页新增按会话(sessionId)聚合的消耗排行,显示调用次数与消耗、相对进度条,便于定位「最烧钱」的对话。
20
+ - **缓存命中列表关键字搜索**:新增搜索框,按 模型 / 服务商 / 会话ID / 用途 实时过滤,与日期筛选叠加生效。
21
+ - **月度预算设置与超支预警**(`lib/index.js` + `lib/client.js`):价格表页新增「月度预算」区块(输入金额保存 / 清除),持久化到数据目录 `budget.json`;概览页 Hero 显示当月已用/预算进度。新增 `setBudget` API,`list` 附带 `budget` 字段(向后兼容)。
22
+ - **说明信息收进可折叠抽屉**:原本首屏固定展示的「本地统计与官方后台的差异 / 高峰·空闲时段说明 / 计价说明」三段说明收进「▸ 帮助与说明」按钮(默认收起),有中断记录时按钮带数量角标;首屏直接展示数据。
23
+ - **底部工具条重组**:导出 CSV / JSON / PNG / 打开目录 / 选择文件导入 / 帮助与说明 合并为一行,导出目标目录输入独立一行。
24
+
25
+ ### 修复 / Fixed
26
+
27
+ - 概览页 Hero 高峰时段徽章颜色修正(高峰 = 橙色)。
28
+
29
+ ### 说明 / Notes
30
+
31
+ - 所有新 UI 文案均已补充英文词典(`__T_EN`),英文界面即时可用。
32
+ - 未引入新 npm 依赖;现有测试 30/30 全部通过。
33
+
34
+ ---
35
+
36
+ ## v1.15.0 (2026-08-29)
37
+
38
+ ### 新增 / New
39
+
40
+ - **用量日历新增日期范围筛选**(`lib/client.js` CalendarView):日历视图此前只有月份导航、无法只看指定区间;现在顶部新增与概览一致的筛选栏(今天 / 近7天 / 近30天 / 全部 + 自定义起止日期)。筛选激活时:
41
+ - **每日消耗统计表跨月显示**范围内所有有记录的天(不再局限于当前月),标题显示所选范围;
42
+ - 顶部「本月调用 / 本月消耗」卡片自动切换为「范围内调用 / 范围内消耗」并按范围汇总(含高峰/空闲拆分);
43
+ - 热力图仅对范围内日期着色,范围外显示为空(`—`);
44
+ - 空态文案随范围显示「该范围内暂无记录。」;
45
+ - 预设按钮切换时自动清空自定义起止日期,避免残留范围误判。
46
+
47
+ ---
48
+
11
49
  ## v1.14.1 (2026-08-29)
12
50
 
13
51
  ### 修复 / Fixed
package/README.md CHANGED
@@ -33,7 +33,7 @@ dsh-usage-plugin is a **usage & cost tracker** plugin in the DeepSeek Harness ec
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
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
- - **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.
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 rollups. Supports **date-range filtering** (Today / 7 days / 30 days / All plus custom start–end dates): when a range is active, the daily stats table shows every day in range across months, the top cards switch to "Calls in range / Cost in range" with range totals, and the heatmap only colors days inside the range.
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
38
  - **Interrupted calls shown truthfully**: calls that were aborted / errored / timed out (e.g. manually stopped generation, stream interruption) are shown with a red **"Interrupted"** badge, the finish reason (Interrupted / Error / Timeout) and a `—` cost. The official console still counts these as API requests and bills their actual tokens, but the harness does not report their usage to the plugin — so the plugin records them at 0 tokens, keeping the **call count aligned with the official console** while costs are unaffected. The Overview's "Calls" card adds a `· interrupted N (not billed)` hint.
39
39
  - **Local stats vs official console**: a fixed notice banner at the top of the panel explains that this panel reflects calls captured locally by the plugin (official prices + peak/off-peak hours), and that the official console (platform.deepseek.com usage page) may show a higher amount because: ① interrupted/failed/timed-out calls are still billed by the console while the plugin records them as 0; ② calls from other API keys on your account (other apps/scripts) do not pass through DeepSeek Harness — the console includes them, the plugin does not; ③ for exact reconciliation, export the official monthly billing CSV and compare. When interrupted calls are detected, the banner also shows a red "Current records include N interrupted call(s) (not billed)" line.
package/README.zh.md CHANGED
@@ -34,7 +34,7 @@ dsh-usage-plugin 是 DeepSeek Harness 生态的**用量与消耗统计插件**
34
34
  > 支持 **Windows / macOS / Linux**:路径按当前平台处理(`node:path`),目录选择与「打开所在目录」均调用系统原生方式(macOS 用 `osascript` / `open`,Linux 用 `zenity` / `xdg-open`),余额查询与导出不依赖 Windows 专用命令。
35
35
 
36
36
  - **用量与消耗**:记录每次模型调用的 token 用量与缓存命中(输入·未命中 / 缓存命中 / 缓存写入 / 输出 / 推理 / 结束原因),按 DeepSeek 峰谷/基础价格计算消耗(工作日高峰时段按北京时间 9:00–12:00、14:00–18:00 计价;自 2026-08-23 起周末全天按空闲价计费)。模型名以请求参数为准如实显示(非 DeepSeek 模型不再显示为「未知模型」,无官方价格的模型消耗按 0 统计)。概览含「按模型」表与「按 API 服务商 × 模型」明细表(每个服务商一组,组内列出各模型的调用与高峰/空闲分列消耗),底部有总费用合计。概览还支持**日期筛选**(今天 / 近 7 天 / 近 30 天 / 全部,外加自定义起止区间),可将聚合统计限定到任意一天或日期范围。
37
- - **用量日历**:按月查看每日用量热力图(按消耗或调用数着色),悬停查看详情(含高峰 / 空闲消耗拆分)、点击某天查看当日调用明细与高峰/空闲消耗统计,附本月每日统计表(高峰消耗 / 空闲消耗 / 总消耗分列)与月度汇总。
37
+ - **用量日历**:按月查看每日用量热力图(按消耗或调用数着色),悬停查看详情(含高峰 / 空闲消耗拆分)、点击某天查看当日调用明细与高峰/空闲消耗统计,附每日统计表(高峰消耗 / 空闲消耗 / 总消耗分列)与汇总。支持**日期范围筛选**(今天 / 近7天 / 近30天 / 全部 + 自定义起止日期):筛选激活时每日统计表跨月显示范围内所有天、顶部卡片切换为「范围内调用 / 范围内消耗」并按范围汇总、热力图仅对范围内日期着色。
38
38
  - **缓存命中列表**:最新记录排在最前,支持 今天 / 近7天 / 近30天 / 全部 快捷筛选与自定义起止日期区间,汇总行与表尾合计区分高峰消耗 / 空闲消耗 / 总费用合计;列表分页渲染(每页 100 条),记录量大也不卡顿。
39
39
  - **中断调用如实呈现**:被中止 / 出错 / 超时(如手动停止生成、流中断)的调用在面板中标红显示「中断」徽标、结束原因(已中断 / 错误 / 超时)与消耗 `—`。这类调用官方后台仍会计入「API 请求次数」并按实际 token 计费,但 harness 不向插件上报 usage——插件按 0 token 记录它们,**调用次数与官方口径一致,消耗不受影响**;概览「调用次数」卡片会提示 `中断 N(未计费)`。
40
40
  - **本地统计 vs 官方后台的差异提示**:用量面板顶部固定显示说明横幅——本面板统计的是插件本地捕获的调用(官方价格 + 峰谷时段),与官方后台(platform.deepseek.com 用量页)相比金额可能更低:① 中断/出错/超时的调用官方仍按实际 token 计费而插件按 0 记录;② 账号下其他 API Key(其它应用/脚本)的调用不经过 DeepSeek Harness,官方包含而插件不包含;③ 精确对账可导出官方月度账单 CSV 对比。检测到中断调用时,横幅额外以红字显示「当前记录中有 N 次中断调用(未计费)」。
package/lib/client.js CHANGED
@@ -21,7 +21,7 @@ window.__ModuleLoader__.load({
21
21
  } catch (e3) {}
22
22
 
23
23
  // ── i18n: zh keys → en dictionary (community addition) ──
24
- var __T_EN = {"本期无消耗(所有用量为 0)":"No usage this turn (all zeros)","按模型":"By model","…及另外 ":"…plus "," 个模型(完整列表见「用量与消耗」页签)":" more models (full list in the Usage & Cost tab)","对话累计":"Conversation total","本轮明细":"This turn","本轮 token":"Turn tokens","本轮消耗":"Turn cost","本轮缓存命中率":"Turn hit rate","本轮耗时":"Turn duration","本轮无记录":"No records this turn","工具调用(内部)":"Tool calls (internal)"," 条内部调用":" internal calls","展开":"Expand","收起":"Collapse","详细记录请在「用量与消耗」页签查看":"Full details are in the Usage & Cost tab","获取中…":"Loading…","总 token":"Total tokens","Token 明细":"Token details","本次输出":"This output","总消耗":"Total cost","耗时":"Run time","缓存命中率":"Cache hit rate","关闭":"Close","无 Token 数据":"No token data","当前 · ":"Current · ","工作日高峰时段":"Weekday peak hours","工作日空闲时段":"Weekday off-peak hours","周末高峰时段":"Weekend peak hours","周末空闲时段":"Weekend off-peak hours","周一至周五 9:00–12:00、14:00–18:00":"Mon–Fri 9:00–12:00 & 14:00–18:00","工作日其余时间":"Other weekday hours","周末(周六、周日)全天按空闲价计费":"All weekend (Sat & Sun) billed at the off-peak rate","2026-08-23 前仍按原规则分峰谷":"Before 2026-08-23 the old peak/valley split still applies","高峰 / 空闲时段说明":"Peak / off-peak hours","工作日高峰":"Weekday peak","空闲时段":"Off-peak hours"," 周一至周五 9:00–12:00、14:00–18:00":" Mon–Fri 9:00–12:00 & 14:00–18:00"," 工作日其余时间,以及周末(周六、周日)全天(自 2026-08-23 起;此前仍按原规则分峰谷)":" Weekday off-peak hours, plus all weekend (Sat & Sun) since 2026-08-23 (before that, the old peak/valley split still applies)","工作日高峰 9:00–12:00、14:00–18:00 · 周末全天空闲价":"Weekday peak 9:00–12:00 & 14:00–18:00 · all-weekend off-peak rate","\n调用 ":"\ncalls ",")\n输入·未命中 ":")\ninput·miss ","\n高峰消耗 ":"\npeak cost ","\n无记录":"\nno records","自动模式:按调用时间自动选择计费档位——新价格表生效前的调用按基础价(旧价格表),生效后的调用按峰谷价(工作日高峰时段 9:00–12:00、14:00–18:00 用高峰价,其余时间——含周末全天——用空闲价)。当前显示:":"Auto mode: the billing tier is chosen automatically by call time — calls made before the new price schedule took effect are billed at base (old) prices; calls after it use peak/valley prices (weekday peak hours 9:00–12:00 & 14:00–18:00 at the peak price, all other times — including all weekend — at the off-peak price). Currently shown:","单位:元 / 百万 tokens。工作日高峰时段(北京时间 9:00–12:00、14:00–18:00)用高峰价,其余时间用空闲价(空闲价 = 高峰价的一半);自 2026-08-23 起周末(周六、周日)全天按空闲价计费,新规则生效前仍按原规则。概览、用量日历、缓存命中列表中的消耗均已按高峰 / 空闲分列统计。":"Unit: CNY per million tokens. Weekday peak hours (Beijing time 9:00–12:00 & 14:00–18:00) use the peak price; all other times use the off-peak price (off-peak = half of peak). Since 2026-08-23, weekends (Sat & Sun) are billed at the off-peak rate all day; calls before the new rule still follow the old split. Costs in Overview, Usage Calendar and the Cache Hit List are split into peak / off-peak accordingly.","未知模型":"Unknown model","未知服务商":"Unknown provider","完成":"Done","工具调用":"Tool call","超长":"Overlong","错误":"Error","已中断":"Interrupted","超时":"Timeout","中断":"Interrupted"," · 中断 ":" · interrupted ","(未计费)":" (not billed)","本地统计与官方后台的差异":"Local stats vs official console","本面板统计的是插件在本地捕获的模型调用(按官方价格与峰谷时段计费)。与官方后台(platform.deepseek.com 用量页)相比,金额可能更低,常见原因:":"This panel shows calls captured locally by the plugin (billed at official prices with peak/off-peak rates). Compared with the official console (platform.deepseek.com usage page), the amount may be lower — common reasons:","① 中断/出错/超时的调用:官方仍按实际 token 计费,插件无法获取其用量,按 0 记录(明细中标「中断」);":"① Interrupted/failed/timed-out calls: the console still bills their actual tokens, but the plugin cannot obtain their usage, so they are recorded as 0 (marked \"Interrupted\" in details);","② 账号下其他 API Key(如其它应用/脚本)的调用不经过 DeepSeek Harness,官方统计包含它们,本插件不包含;":"② Calls from other API keys on your account (e.g. other apps/scripts) do not pass through DeepSeek Harness — the console includes them, this plugin does not;","③ 如需精确对账,可在官方用量页导出月度账单 CSV 与本插件对比。":"③ For exact reconciliation, export the monthly billing CSV from the official usage page and compare it with this plugin.","当前记录中有 ":"Current records include "," 次中断调用(未计费)——对应官方后台已计费的部分。":" interrupted call(s) (not billed) — corresponding to the part already billed by the official console.","自动(生效前基础价 · 生效后峰谷价)":"Auto (base price before effective date · peak/valley after)","基础价格":"Base price","峰谷价格":"Peak/valley price","自动":"Auto","canvas 2d 不可用":"canvas 2D unavailable","用量与消耗报告":"Usage & Cost Report","生成时间 ":"Generated ","(北京) · 共 ":" (Beijing) · total "," 条调用 · 缓存命中率 ":" calls · cache-hit rate "," · 高峰价消耗 ":" · peak-rate cost "," · 空闲价消耗 ":" · off-peak cost "," · 合计 ":" · total "," · 报告仅含最近 ":" · report limited to the latest "," 条":" records","调用次数":"Calls","输入 · 未命中":"Input · miss","输入 · 缓存命中":"Input · cache hit","输出":"Output","高峰消耗":"Peak cost","空闲消耗":"Off-peak cost","总消耗 (峰谷价)":"Total cost (peak/valley)","缓存命中列表(共 ":"Cache hit list ("," 条)":" records)","时间(北京)":"Time (Beijing)","模型":"Model","输入·未命中":"Input·miss","缓存命中":"Cache hit","缓存写入":"Cache write","推理":"Reasoning","命中率":"Hit rate","时段":"Period","结束":"End","消耗(峰谷)":"Cost (peak/valley)","峰":"Peak","谷":"Valley","消耗表(按模型 · 峰谷价 · 高峰/空闲分列)":"Cost table (by model · peak/valley prices)","调用":"Calls","总消耗":"Total cost","合计":"Total","消耗表(按 API 服务商 × 模型 · 峰谷价)":"Cost table (by API provider × model · peak/valley)","API 服务商 / 模型":"API provider / model"," 个模型)":" models)","总费用合计":"Total cost overall","计价按请求的 API 服务商分别应用已核验价格;DigitalOcean 美元价按 USD/CNY 汇率折算;无法可靠映射的第三方价格按 ¥0。":"Billing applies verified prices per requesting API provider; DigitalOcean USD prices are converted at the USD/CNY rate; third-party prices that cannot be mapped reliably are counted as ¥0."," 个服务商":" providers","今天":"Today","近7天":"Last 7 days","近30天":"Last 30 days","全部":"All","自定义区间:":"Custom range:","至":"to","清除筛选":"Clear filters","当前范围:共 ":"Current range: ","(全部记录)":"(all records)"," · 总消耗 ":" · total cost ","高峰 ":"Peak "," · 空闲 ":" · off-peak ","命中率 ":"hit rate ","高峰时段 9:00–12:00、14:00–18:00":"Peak hours 9:00–12:00 & 14:00–18:00","其余空闲时段":"other hours are off-peak","高峰 + 空闲 · ":"Peak + off-peak · ","消耗表(按模型)":"Cost table (by model)","API 服务商":"API provider","高峰/空闲":"Peak/Off-peak","消耗明细(按 API 服务商 × 模型)":"Cost breakdown (by API provider × model)","DeepSeek 官方请求按官方峰谷价;SiliconFlow 按自身人民币公开价;DigitalOcean 按美元公开价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。模型名与 API 服务商均以请求参数为准。":"DeepSeek official requests use official peak/valley prices; SiliconFlow uses its own public CNY prices; DigitalOcean uses public USD prices converted to CNY at the USD/CNY rate; Qwen is not billed yet; AMD GPU Cloud DeepSeek V4 Flash is free at ¥0. Model names and API providers follow the actual request parameters."," 次(高峰 ":" calls (peak "," / 空闲 ":" / off-peak "," · 缓存命中 ":" · cache hit "," · 输出 ":" · output "," · 推理 ":" · reasoning "," · 空闲消耗 ":" · off-peak cost ","‹ 上月":"‹ Prev month","下月 ›":"Next month ›","按消耗":"By cost","按调用数":"By calls","热力:":"Heatmap: ","低 → 高(":"low → high (","当日消耗":"Day cost","当日调用数":"Day calls","· 悬停查看详情,点击某天查看当日调用":"· hover for details, click a day for that day's calls","本月调用":"Calls this month","本月消耗":"Cost this month","本月暂无记录。":"No records this month.","日期":"Date","总消耗(自动)":"Total cost (auto)"," 调用明细":" call details"," 条 · ":" records · "," | 空闲 ":" | off-peak "," | 合计 ":" | total ","该日无记录。":"No records for this day.","消耗":"Cost","清除区间":"Clear range"," 条 · 输入·未命中 ":" records · input·miss "," · 缓存写入 ":" · cache write "," · 高峰消耗 ":" · peak cost ","该时间范围内没有记录。":"No records in this range.","‹ 上一页":"‹ Prev page","第 ":"Page "," 页 · 每页 ":" page · per page "," 条 · 共 ":" records · total ","下一页 ›":"Next page ›","新价格表生效时间未知":"New price schedule effective date unknown","新价格表(峰谷价)将于 ":"The new peak/valley price schedule takes effect ","(北京时间)生效,当前按旧价格表(基础价)计费":" (Beijing time); currently billing on the old base-price schedule","新价格表(峰谷价)已生效(自 ":"New peak/valley schedule in effect (since "," 起)":")","今日 $ / ¥ 汇率":"Today's $/¥ rate","暂不可用":"Temporarily unavailable","汇率日期 ":"Rate date ","等待获取":"Pending fetch","汇率状态":"Rate status","缓存汇率":"Cached rate","最新可用":"Latest available","获取失败":"Fetch failed","刷新汇率":"Refresh rate","美元计价的 DigitalOcean 消耗按该 USD/CNY 汇率换算成人民币;人民币定价不受汇率影响。":"USD-denominated DigitalOcean costs are converted to CNY at this rate; CNY pricing is unaffected by it.","DeepSeek 官方 API 价格表":"DeepSeek Official API Price Table","本表仅用于 DeepSeek 官方 Provider;第三方 Provider 按下方已核验的自身价格计费,无法可靠匹配价格的模型按 0 统计。价格按官方公布固定,不可编辑。":"This table applies only to the official DeepSeek provider; third-party providers bill at their own verified prices below, and models without a reliable price match are counted as 0. Prices are fixed as officially published and are not editable.","峰谷价":"Peak/valley","基础价":"Base","自动模式:按调用时间自动选择计费档位——新价格表生效前的调用按基础价(旧价格表),生效后的调用按峰谷价(高峰时段 9:00–12:00、14:00–18:00 用高峰价,其余空闲时段用空闲价)。当前显示:":"Auto mode: the billing tier is chosen automatically by call time — calls made before the new schedule took effect use base (old) prices, calls after it use peak/valley prices (peak hours 9:00–12:00 & 14:00–18:00 at peak price, other hours at off-peak). Currently shown:","基础价表(旧价格表,新价格生效前)":"Base price table (old schedule, before new prices take effect)","峰谷价表(新价格表)":"Peak/valley table (new schedule)","。价格按官方公布固定,不可编辑。":". Prices are fixed as officially published and are not editable.","缓存命中(输入)":"Cache hit (input)","空闲 · 缓存命中":"Off-peak · cache hit","空闲 · 输入":"Off-peak · input","空闲 · 输出":"Off-peak · output","高峰 · 缓存命中":"Peak · cache hit","高峰 · 输入":"Peak · input","高峰 · 输出":"Peak · output","单位:元 / 百万 tokens。高峰时段(北京时间 9:00–12:00、14:00–18:00)用高峰价,其余空闲时段用空闲价(空闲价 = 高峰价的一半)。概览、用量日历、缓存命中列表中的消耗均已按高峰 / 空闲分列统计。":"Unit: CNY per million tokens. Peak hours (Beijing 9:00–12:00 & 14:00–18:00) use peak prices; other hours use off-peak (= half of peak). Costs in Overview, Usage Calendar and Cache Hit List are split into peak / off-peak accordingly.","第三方平台价格与覆盖状态(2026-08-19 核验)":"Third-party platform prices & coverage (verified 2026-08-19)","只把能与记录中的 provider/model 可靠匹配的价格用于自动计费;美元价格先按 USD 计算,再乘 USD/CNY 汇率统一折算成人民币。":"Only prices that reliably match a record's provider/model are used for automatic billing; USD prices are computed in USD first, then converted to CNY at the USD/CNY rate.","模型 / 状态":"Model / status","输入":"Input","说明":"Notes","已用于自动计费":"Used for auto billing","缓存命中价按 2026-08-03 起生效公告":"Cache-hit price per the notice effective 2026-08-03","按输入价":"Billed at input price","公告未单列缓存命中价":"The notice lists no separate cache-hit price","自动按 USD/CNY 汇率换算人民币并计费":"Auto-converted to CNY at the USD/CNY rate and billed","Alibaba / 千问":"Alibaba / Qwen","暂不计费":"Not billed yet","调用与 token 正常统计;费用暂按 ¥0":"Calls and tokens tracked normally; cost counted as ¥0 for now","免费;费用固定按 ¥0":"Free; cost fixed at ¥0","DigitalOcean 使用美元官方价并按最新可用 USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud 的 DeepSeek V4 Flash 免费按 ¥0。":"DigitalOcean bills at official USD prices converted via the latest available USD/CNY rate; Qwen is not billed yet; AMD GPU Cloud DeepSeek V4 Flash is free at ¥0.","概览":"Overview","用量日历":"Usage Calendar","缓存命中列表":"Cache Hit List","价格表":"Price Table","已导出:":"Exported: ","导出失败:":"Export failed: ","未知错误":"unknown error","导出中…":"Exporting…","生成图片中…":"Rendering image…","画布不可用":"Canvas unavailable","生成图片失败:":"Image rendering failed: ","打开目录选择…":"Open directory picker…","导出目标:":"Export destination:","选择目录失败:":"Failed to pick directory: ","打开文件夹失败:":"Failed to open folder: ","读取文件…":"Reading file…","导入成功:新增 ":"Import OK: added "," 条,跳过重复 ":", skipped duplicates "," 条,忽略无效 ":", ignored invalid "," 条,现有共 ":", total now ","导入失败:":"Import failed: ","读取文件失败":"Failed to read file","用量与消耗":"Usage & Cost","记录插件激活后的每一次模型调用":"Logs every model call since the plugin was activated","当前 · 高峰时段":"Current · peak hours","当前 · 空闲时段":"Current · off-peak hours","新价格已生效":"New prices in effect","新价格未生效":"New prices not yet in effect","刷新":"Refresh","清空":"Clear","计价说明:DeepSeek 官方与 SiliconFlow 按人民币价格;DigitalOcean 按美元官方价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。":"Billing notes: DeepSeek official and SiliconFlow bill in CNY; DigitalOcean bills at official USD prices × USD/CNY rate; Qwen is not billed yet; AMD GPU Cloud DeepSeek V4 Flash is free at ¥0.","导出 CSV":"Export CSV","导出 JSON":"Export JSON","导出图片 (PNG)":"Export image (PNG)","打开目录":"Open folder","导出目标目录(留空 = 默认数据目录)":"Export target directory (empty = default data dir)","选择目录…":"Choose directory…","已恢复默认数据目录":"Restored default data directory","重置":"Reset","选择文件导入":"Choose a file to import","数据持久化:":"Data persistence:","(每次调用实时落盘,插件重启后自动恢复,最多保留 100000 条)":"(every call is written to disk live, restored automatically after restart, up to 100000 records)","持久化未启用:":"Persistence disabled:","未知原因":"unknown reason","DEEPSEEK_API_KEY(推理 Key)":"DEEPSEEK_API_KEY (inference key)","模型设置中 Provider ID 或显示名为 siliconflow 的提供商 API Key":"API key of the provider whose Provider ID or display name is siliconflow in model settings","打开 SiliconFlow API 密钥":"Open SiliconFlow API keys","账户级 dop_v1_ Personal Access Token;Read Only(api:read)或 billing:read":"Account-level dop_v1_ Personal Access Token; Read Only (api:read) or billing:read","创建 DigitalOcean Account API Token":"Create a DigitalOcean account API token","暂无公开余额 API,仅支持控制台查看":"No public balance API yet — console only","打开 AMD Developer Cloud":"Open AMD Developer Cloud","查询失败":"Query failed","无法读取凭据状态":"Cannot read credential status","请输入以 dop_v1_ 开头的 DigitalOcean 账户 PAT。":"Enter a DigitalOcean account PAT starting with dop_v1_.","保存失败":"Save failed","使用凭据 ":"Using credential ","来源 ":"source ","模型提供商 ":"Model provider ","端点 ":"Endpoint "," · 账户余额":" · account balance","USD · 预付款/信用余额":"USD · prepaid/credit balance","USD · 待结算金额":"USD · pending amount","USD · 当前无余额":"USD · no balance"," · 账户可用":" · account available"," · 账户不可用":" · account unavailable"," · 查询成功":" · query succeeded","剩余余额查询":"Remaining balance query","查询 DeepSeek、SiliconFlow 与 DigitalOcean 账户余额":"Query DeepSeek, SiliconFlow and DigitalOcean account balances","打开控制台":"Open console","查询中…":"Querying…","查询账单":"Query billing","查询余额":"Query balance","所需凭据:":"Required credentials:","仅支持控制台查看":"Console only","插件不会尝试未经公开文档确认的端点,也不会把 AMD 推理 Key 当作账单凭据。":"The plugin never probes endpoints unconfirmed by public docs, and never uses AMD inference keys as billing credentials.","请确认凭据类型正确、权限包含余额/账单读取,且网络可访问服务商官方 API。":"Please verify the credential type is correct, its permissions include balance/billing read, and the provider's official API is reachable from this network.","打开服务商凭据页面":"Open provider credentials page","正在查询余额…":"Querying balance…","可用":"Available","不可用":"Unavailable","账单更新时间":"Billing updated","查询时间":"Queried at","北京时间":"Beijing time","请创建账户级 Personal Access Token。可选 Read Only(api:read,包含 billing:read),或自定义 billing:read;不要使用 Gradient AI 推理 Key。":"Please create an account-level Personal Access Token. Choose Read Only (api:read incl. billing:read) or a custom scope with billing:read; do not use a Gradient AI inference key.","正在检查已保存的 Token…":"Checking saved token…","已保存:":"Saved: ","(来源:":" (source: ","无法读取 Token 状态:":"Cannot read token status: ","尚未保存 DIGITALOCEAN_TOKEN。":"DIGITALOCEAN_TOKEN is not saved yet.","输入新的 dop_v1_ Token 可替换(当前值已隐藏)":"Enter a new dop_v1_ token to replace it (current value hidden)","保存中…":"Saving…","保存并查询":"Save & query","当前 Token 来自只读环境变量;请在原来源中修改,页面不会覆盖它。":"The current token comes from a read-only environment variable; edit it at its source — this panel will not overwrite it.","Token 已安全保存;页面和 API 响应不会回传明文。":"Token stored securely; neither the page nor API responses echo it back.","SiliconFlow 查询说明":"SiliconFlow query notes","公开 API 已成功返回 ¥0.00。SiliconFlow 的 /v1/user/info 当前不返回代金券或历史用量;控制台可用总额可能非零。这里忠实展示 API 原始余额字段,不把它等同于控制台完整额度。":"The public API returned ¥0.00 successfully. SiliconFlow's /v1/user/info currently returns no vouchers or historical usage; the console total may still be non-zero. The raw balance field from the API is shown faithfully here and should not be read as the full console credit.","余额卡片展示 SiliconFlow /v1/user/info 公开 API 返回的原始余额字段。":"The balance card shows the raw balance field returned by SiliconFlow's public /v1/user/info API.","返回字段":"Returned fields","凭据读取规则":"Credential lookup rules","插件只检查“设置 → 模型”中 Provider ID 或显示名为 siliconflow 的提供商,并读取其 apiKeyEnv 对应的已保存 API Key。":"The plugin only checks providers in Settings → Models whose Provider ID or display name is siliconflow, and reads the saved API key referenced by their apiKeyEnv.","如果未找到提供商、未填写 API Key 或凭据引用失效,插件会停止查询并说明需要修复的配置;不会回退到其他服务商的 Key。":"If no such provider exists, the key is empty, or the credential reference is broken, the plugin stops querying and explains what needs fixing; it never falls back to another provider's key.","DigitalOcean 查询的是主账户 Billing API;DigitalOcean AI 推理 Key 与 AMD GPU Cloud 推理 Key 均不能直接查询账单。":"DigitalOcean queries the main account Billing API; DigitalOcean AI inference keys and AMD GPU Cloud inference keys cannot query billing directly.","\n调用 ":"\ncalls ",")\n输入·未命中 ":")\ninput·miss ","\n高峰消耗 ":"\npeak cost ","\n无记录":"\nno records","查询 DeepSeek、SiliconFlow、DigitalOcean 与百炼 Token Plan":"Query DeepSeek, SiliconFlow, DigitalOcean & Bailian Token Plan balances","百炼 Token Plan · 1 周配额":"Bailian Token Plan · 1-week quota","百炼 Token Plan 配额用量":"Bailian Token Plan quota usage","百炼 Token Plan":"Bailian Token Plan","本周配额":"This week's quota","开始":"Start","结束":"End","已用 ":"Used ","百炼 Token Plan 查询说明":"Bailian Token Plan query notes","本查询复用百炼 CLI(bl)的“控制台登录”OAuth token,不需要阿里云 AccessKey,权限面很小。":"This query reuses the console-login OAuth token from the Bailian CLI (bl); no Alibaba Cloud AccessKey is needed and the permission scope is minimal.","如尚未登录,请先在终端执行:bl auth login --console,然后回到本页点“查询余额”。":"If you haven't logged in yet, run: bl auth login --console, then come back and click 'Query balance'.","打开百炼 CLI 登录说明":"Open Bailian CLI login docs","百炼控制台 OAuth 登录(~/.bailian/config.json 的 access_token,用 bl auth login --console 生成)":"Bailian console OAuth login (access_token in ~/.bailian/config.json, created by bl auth login --console)","数据来自阿里云百炼控制台内部门户网关(zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage),展示的是 1 周配额已用进度与周期。":"Data from Alibaba Cloud Bailian console portal gateway (zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage), showing 1-week quota usage progress and period."};
24
+ var __T_EN = {"本期无消耗(所有用量为 0)":"No usage this turn (all zeros)","按模型":"By model","…及另外 ":"…plus "," 个模型(完整列表见「用量与消耗」页签)":" more models (full list in the Usage & Cost tab)","对话累计":"Conversation total","本轮明细":"This turn","本轮 token":"Turn tokens","本轮消耗":"Turn cost","本轮缓存命中率":"Turn hit rate","本轮耗时":"Turn duration","本轮无记录":"No records this turn","工具调用(内部)":"Tool calls (internal)"," 条内部调用":" internal calls","展开":"Expand","收起":"Collapse","详细记录请在「用量与消耗」页签查看":"Full details are in the Usage & Cost tab","获取中…":"Loading…","总 token":"Total tokens","Token 明细":"Token details","本次输出":"This output","总消耗":"Total cost","耗时":"Run time","缓存命中率":"Cache hit rate","关闭":"Close","无 Token 数据":"No token data","当前 · ":"Current · ","工作日高峰时段":"Weekday peak hours","工作日空闲时段":"Weekday off-peak hours","周末高峰时段":"Weekend peak hours","周末空闲时段":"Weekend off-peak hours","周一至周五 9:00–12:00、14:00–18:00":"Mon–Fri 9:00–12:00 & 14:00–18:00","工作日其余时间":"Other weekday hours","周末(周六、周日)全天按空闲价计费":"All weekend (Sat & Sun) billed at the off-peak rate","2026-08-23 前仍按原规则分峰谷":"Before 2026-08-23 the old peak/valley split still applies","高峰 / 空闲时段说明":"Peak / off-peak hours","工作日高峰":"Weekday peak","空闲时段":"Off-peak hours"," 周一至周五 9:00–12:00、14:00–18:00":" Mon–Fri 9:00–12:00 & 14:00–18:00"," 工作日其余时间,以及周末(周六、周日)全天(自 2026-08-23 起;此前仍按原规则分峰谷)":" Weekday off-peak hours, plus all weekend (Sat & Sun) since 2026-08-23 (before that, the old peak/valley split still applies)","工作日高峰 9:00–12:00、14:00–18:00 · 周末全天空闲价":"Weekday peak 9:00–12:00 & 14:00–18:00 · all-weekend off-peak rate","\n调用 ":"\ncalls ",")\n输入·未命中 ":")\ninput·miss ","\n高峰消耗 ":"\npeak cost ","\n无记录":"\nno records","自动模式:按调用时间自动选择计费档位——新价格表生效前的调用按基础价(旧价格表),生效后的调用按峰谷价(工作日高峰时段 9:00–12:00、14:00–18:00 用高峰价,其余时间——含周末全天——用空闲价)。当前显示:":"Auto mode: the billing tier is chosen automatically by call time — calls made before the new price schedule took effect are billed at base (old) prices; calls after it use peak/valley prices (weekday peak hours 9:00–12:00 & 14:00–18:00 at the peak price, all other times — including all weekend — at the off-peak price). Currently shown:","单位:元 / 百万 tokens。工作日高峰时段(北京时间 9:00–12:00、14:00–18:00)用高峰价,其余时间用空闲价(空闲价 = 高峰价的一半);自 2026-08-23 起周末(周六、周日)全天按空闲价计费,新规则生效前仍按原规则。概览、用量日历、缓存命中列表中的消耗均已按高峰 / 空闲分列统计。":"Unit: CNY per million tokens. Weekday peak hours (Beijing time 9:00–12:00 & 14:00–18:00) use the peak price; all other times use the off-peak price (off-peak = half of peak). Since 2026-08-23, weekends (Sat & Sun) are billed at the off-peak rate all day; calls before the new rule still follow the old split. Costs in Overview, Usage Calendar and the Cache Hit List are split into peak / off-peak accordingly.","未知模型":"Unknown model","未知服务商":"Unknown provider","完成":"Done","工具调用":"Tool call","超长":"Overlong","错误":"Error","已中断":"Interrupted","超时":"Timeout","中断":"Interrupted"," · 中断 ":" · interrupted ","(未计费)":" (not billed)","本地统计与官方后台的差异":"Local stats vs official console","本面板统计的是插件在本地捕获的模型调用(按官方价格与峰谷时段计费)。与官方后台(platform.deepseek.com 用量页)相比,金额可能更低,常见原因:":"This panel shows calls captured locally by the plugin (billed at official prices with peak/off-peak rates). Compared with the official console (platform.deepseek.com usage page), the amount may be lower — common reasons:","① 中断/出错/超时的调用:官方仍按实际 token 计费,插件无法获取其用量,按 0 记录(明细中标「中断」);":"① Interrupted/failed/timed-out calls: the console still bills their actual tokens, but the plugin cannot obtain their usage, so they are recorded as 0 (marked \"Interrupted\" in details);","② 账号下其他 API Key(如其它应用/脚本)的调用不经过 DeepSeek Harness,官方统计包含它们,本插件不包含;":"② Calls from other API keys on your account (e.g. other apps/scripts) do not pass through DeepSeek Harness — the console includes them, this plugin does not;","③ 如需精确对账,可在官方用量页导出月度账单 CSV 与本插件对比。":"③ For exact reconciliation, export the monthly billing CSV from the official usage page and compare it with this plugin.","当前记录中有 ":"Current records include "," 次中断调用(未计费)——对应官方后台已计费的部分。":" interrupted call(s) (not billed) — corresponding to the part already billed by the official console.","范围内调用":"Calls in range","范围内消耗":"Cost in range","该范围内暂无记录。":"No records in this range.","自动(生效前基础价 · 生效后峰谷价)":"Auto (base price before effective date · peak/valley after)","基础价格":"Base price","峰谷价格":"Peak/valley price","自动":"Auto","canvas 2d 不可用":"canvas 2D unavailable","用量与消耗报告":"Usage & Cost Report","生成时间 ":"Generated ","(北京) · 共 ":" (Beijing) · total "," 条调用 · 缓存命中率 ":" calls · cache-hit rate "," · 高峰价消耗 ":" · peak-rate cost "," · 空闲价消耗 ":" · off-peak cost "," · 合计 ":" · total "," · 报告仅含最近 ":" · report limited to the latest "," 条":" records","调用次数":"Calls","输入 · 未命中":"Input · miss","输入 · 缓存命中":"Input · cache hit","输出":"Output","高峰消耗":"Peak cost","空闲消耗":"Off-peak cost","总消耗 (峰谷价)":"Total cost (peak/valley)","缓存命中列表(共 ":"Cache hit list ("," 条)":" records)","时间(北京)":"Time (Beijing)","模型":"Model","输入·未命中":"Input·miss","缓存命中":"Cache hit","缓存写入":"Cache write","推理":"Reasoning","命中率":"Hit rate","时段":"Period","结束":"End","消耗(峰谷)":"Cost (peak/valley)","峰":"Peak","谷":"Valley","消耗表(按模型 · 峰谷价 · 高峰/空闲分列)":"Cost table (by model · peak/valley prices)","调用":"Calls","总消耗":"Total cost","合计":"Total","消耗表(按 API 服务商 × 模型 · 峰谷价)":"Cost table (by API provider × model · peak/valley)","API 服务商 / 模型":"API provider / model"," 个模型)":" models)","总费用合计":"Total cost overall","计价按请求的 API 服务商分别应用已核验价格;DigitalOcean 美元价按 USD/CNY 汇率折算;无法可靠映射的第三方价格按 ¥0。":"Billing applies verified prices per requesting API provider; DigitalOcean USD prices are converted at the USD/CNY rate; third-party prices that cannot be mapped reliably are counted as ¥0."," 个服务商":" providers","今天":"Today","近7天":"Last 7 days","近30天":"Last 30 days","全部":"All","自定义区间:":"Custom range:","至":"to","清除筛选":"Clear filters","当前范围:共 ":"Current range: ","(全部记录)":"(all records)"," · 总消耗 ":" · total cost ","高峰 ":"Peak "," · 空闲 ":" · off-peak ","命中率 ":"hit rate ","高峰时段 9:00–12:00、14:00–18:00":"Peak hours 9:00–12:00 & 14:00–18:00","其余空闲时段":"other hours are off-peak","高峰 + 空闲 · ":"Peak + off-peak · ","消耗表(按模型)":"Cost table (by model)","API 服务商":"API provider","高峰/空闲":"Peak/Off-peak","消耗明细(按 API 服务商 × 模型)":"Cost breakdown (by API provider × model)","DeepSeek 官方请求按官方峰谷价;SiliconFlow 按自身人民币公开价;DigitalOcean 按美元公开价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。模型名与 API 服务商均以请求参数为准。":"DeepSeek official requests use official peak/valley prices; SiliconFlow uses its own public CNY prices; DigitalOcean uses public USD prices converted to CNY at the USD/CNY rate; Qwen is not billed yet; AMD GPU Cloud DeepSeek V4 Flash is free at ¥0. Model names and API providers follow the actual request parameters."," 次(高峰 ":" calls (peak "," / 空闲 ":" / off-peak "," · 缓存命中 ":" · cache hit "," · 输出 ":" · output "," · 推理 ":" · reasoning "," · 空闲消耗 ":" · off-peak cost ","‹ 上月":"‹ Prev month","下月 ›":"Next month ›","按消耗":"By cost","按调用数":"By calls","热力:":"Heatmap: ","低 → 高(":"low → high (","当日消耗":"Day cost","当日调用数":"Day calls","· 悬停查看详情,点击某天查看当日调用":"· hover for details, click a day for that day's calls","本月调用":"Calls this month","本月消耗":"Cost this month","本月暂无记录。":"No records this month.","日期":"Date","总消耗(自动)":"Total cost (auto)"," 调用明细":" call details"," 条 · ":" records · "," | 空闲 ":" | off-peak "," | 合计 ":" | total ","该日无记录。":"No records for this day.","消耗":"Cost","清除区间":"Clear range"," 条 · 输入·未命中 ":" records · input·miss "," · 缓存写入 ":" · cache write "," · 高峰消耗 ":" · peak cost ","该时间范围内没有记录。":"No records in this range.","‹ 上一页":"‹ Prev page","第 ":"Page "," 页 · 每页 ":" page · per page "," 条 · 共 ":" records · total ","下一页 ›":"Next page ›","新价格表生效时间未知":"New price schedule effective date unknown","新价格表(峰谷价)将于 ":"The new peak/valley price schedule takes effect ","(北京时间)生效,当前按旧价格表(基础价)计费":" (Beijing time); currently billing on the old base-price schedule","新价格表(峰谷价)已生效(自 ":"New peak/valley schedule in effect (since "," 起)":")","今日 $ / ¥ 汇率":"Today's $/¥ rate","暂不可用":"Temporarily unavailable","汇率日期 ":"Rate date ","等待获取":"Pending fetch","汇率状态":"Rate status","缓存汇率":"Cached rate","最新可用":"Latest available","获取失败":"Fetch failed","刷新汇率":"Refresh rate","美元计价的 DigitalOcean 消耗按该 USD/CNY 汇率换算成人民币;人民币定价不受汇率影响。":"USD-denominated DigitalOcean costs are converted to CNY at this rate; CNY pricing is unaffected by it.","DeepSeek 官方 API 价格表":"DeepSeek Official API Price Table","本表仅用于 DeepSeek 官方 Provider;第三方 Provider 按下方已核验的自身价格计费,无法可靠匹配价格的模型按 0 统计。价格按官方公布固定,不可编辑。":"This table applies only to the official DeepSeek provider; third-party providers bill at their own verified prices below, and models without a reliable price match are counted as 0. Prices are fixed as officially published and are not editable.","峰谷价":"Peak/valley","基础价":"Base","自动模式:按调用时间自动选择计费档位——新价格表生效前的调用按基础价(旧价格表),生效后的调用按峰谷价(高峰时段 9:00–12:00、14:00–18:00 用高峰价,其余空闲时段用空闲价)。当前显示:":"Auto mode: the billing tier is chosen automatically by call time — calls made before the new schedule took effect use base (old) prices, calls after it use peak/valley prices (peak hours 9:00–12:00 & 14:00–18:00 at peak price, other hours at off-peak). Currently shown:","基础价表(旧价格表,新价格生效前)":"Base price table (old schedule, before new prices take effect)","峰谷价表(新价格表)":"Peak/valley table (new schedule)","。价格按官方公布固定,不可编辑。":". Prices are fixed as officially published and are not editable.","缓存命中(输入)":"Cache hit (input)","空闲 · 缓存命中":"Off-peak · cache hit","空闲 · 输入":"Off-peak · input","空闲 · 输出":"Off-peak · output","高峰 · 缓存命中":"Peak · cache hit","高峰 · 输入":"Peak · input","高峰 · 输出":"Peak · output","单位:元 / 百万 tokens。高峰时段(北京时间 9:00–12:00、14:00–18:00)用高峰价,其余空闲时段用空闲价(空闲价 = 高峰价的一半)。概览、用量日历、缓存命中列表中的消耗均已按高峰 / 空闲分列统计。":"Unit: CNY per million tokens. Peak hours (Beijing 9:00–12:00 & 14:00–18:00) use peak prices; other hours use off-peak (= half of peak). Costs in Overview, Usage Calendar and Cache Hit List are split into peak / off-peak accordingly.","第三方平台价格与覆盖状态(2026-08-19 核验)":"Third-party platform prices & coverage (verified 2026-08-19)","只把能与记录中的 provider/model 可靠匹配的价格用于自动计费;美元价格先按 USD 计算,再乘 USD/CNY 汇率统一折算成人民币。":"Only prices that reliably match a record's provider/model are used for automatic billing; USD prices are computed in USD first, then converted to CNY at the USD/CNY rate.","模型 / 状态":"Model / status","输入":"Input","说明":"Notes","已用于自动计费":"Used for auto billing","缓存命中价按 2026-08-03 起生效公告":"Cache-hit price per the notice effective 2026-08-03","按输入价":"Billed at input price","公告未单列缓存命中价":"The notice lists no separate cache-hit price","自动按 USD/CNY 汇率换算人民币并计费":"Auto-converted to CNY at the USD/CNY rate and billed","Alibaba / 千问":"Alibaba / Qwen","暂不计费":"Not billed yet","调用与 token 正常统计;费用暂按 ¥0":"Calls and tokens tracked normally; cost counted as ¥0 for now","免费;费用固定按 ¥0":"Free; cost fixed at ¥0","DigitalOcean 使用美元官方价并按最新可用 USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud 的 DeepSeek V4 Flash 免费按 ¥0。":"DigitalOcean bills at official USD prices converted via the latest available USD/CNY rate; Qwen is not billed yet; AMD GPU Cloud DeepSeek V4 Flash is free at ¥0.","概览":"Overview","用量日历":"Usage Calendar","缓存命中列表":"Cache Hit List","价格表":"Price Table","已导出:":"Exported: ","导出失败:":"Export failed: ","未知错误":"unknown error","导出中…":"Exporting…","生成图片中…":"Rendering image…","画布不可用":"Canvas unavailable","生成图片失败:":"Image rendering failed: ","打开目录选择…":"Open directory picker…","导出目标:":"Export destination:","选择目录失败:":"Failed to pick directory: ","打开文件夹失败:":"Failed to open folder: ","读取文件…":"Reading file…","导入成功:新增 ":"Import OK: added "," 条,跳过重复 ":", skipped duplicates "," 条,忽略无效 ":", ignored invalid "," 条,现有共 ":", total now ","导入失败:":"Import failed: ","读取文件失败":"Failed to read file","用量与消耗":"Usage & Cost","记录插件激活后的每一次模型调用":"Logs every model call since the plugin was activated","当前 · 高峰时段":"Current · peak hours","当前 · 空闲时段":"Current · off-peak hours","新价格已生效":"New prices in effect","新价格未生效":"New prices not yet in effect","刷新":"Refresh","清空":"Clear","计价说明:DeepSeek 官方与 SiliconFlow 按人民币价格;DigitalOcean 按美元官方价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。":"Billing notes: DeepSeek official and SiliconFlow bill in CNY; DigitalOcean bills at official USD prices × USD/CNY rate; Qwen is not billed yet; AMD GPU Cloud DeepSeek V4 Flash is free at ¥0.","导出 CSV":"Export CSV","导出 JSON":"Export JSON","导出图片 (PNG)":"Export image (PNG)","打开目录":"Open folder","导出目标目录(留空 = 默认数据目录)":"Export target directory (empty = default data dir)","选择目录…":"Choose directory…","已恢复默认数据目录":"Restored default data directory","重置":"Reset","选择文件导入":"Choose a file to import","数据持久化:":"Data persistence:","(每次调用实时落盘,插件重启后自动恢复,最多保留 100000 条)":"(every call is written to disk live, restored automatically after restart, up to 100000 records)","持久化未启用:":"Persistence disabled:","未知原因":"unknown reason","DEEPSEEK_API_KEY(推理 Key)":"DEEPSEEK_API_KEY (inference key)","模型设置中 Provider ID 或显示名为 siliconflow 的提供商 API Key":"API key of the provider whose Provider ID or display name is siliconflow in model settings","打开 SiliconFlow API 密钥":"Open SiliconFlow API keys","账户级 dop_v1_ Personal Access Token;Read Only(api:read)或 billing:read":"Account-level dop_v1_ Personal Access Token; Read Only (api:read) or billing:read","创建 DigitalOcean Account API Token":"Create a DigitalOcean account API token","暂无公开余额 API,仅支持控制台查看":"No public balance API yet — console only","打开 AMD Developer Cloud":"Open AMD Developer Cloud","查询失败":"Query failed","无法读取凭据状态":"Cannot read credential status","请输入以 dop_v1_ 开头的 DigitalOcean 账户 PAT。":"Enter a DigitalOcean account PAT starting with dop_v1_.","保存失败":"Save failed","使用凭据 ":"Using credential ","来源 ":"source ","模型提供商 ":"Model provider ","端点 ":"Endpoint "," · 账户余额":" · account balance","USD · 预付款/信用余额":"USD · prepaid/credit balance","USD · 待结算金额":"USD · pending amount","USD · 当前无余额":"USD · no balance"," · 账户可用":" · account available"," · 账户不可用":" · account unavailable"," · 查询成功":" · query succeeded","剩余余额查询":"Remaining balance query","查询 DeepSeek、SiliconFlow 与 DigitalOcean 账户余额":"Query DeepSeek, SiliconFlow and DigitalOcean account balances","打开控制台":"Open console","查询中…":"Querying…","查询账单":"Query billing","查询余额":"Query balance","所需凭据:":"Required credentials:","仅支持控制台查看":"Console only","插件不会尝试未经公开文档确认的端点,也不会把 AMD 推理 Key 当作账单凭据。":"The plugin never probes endpoints unconfirmed by public docs, and never uses AMD inference keys as billing credentials.","请确认凭据类型正确、权限包含余额/账单读取,且网络可访问服务商官方 API。":"Please verify the credential type is correct, its permissions include balance/billing read, and the provider's official API is reachable from this network.","打开服务商凭据页面":"Open provider credentials page","正在查询余额…":"Querying balance…","可用":"Available","不可用":"Unavailable","账单更新时间":"Billing updated","查询时间":"Queried at","北京时间":"Beijing time","请创建账户级 Personal Access Token。可选 Read Only(api:read,包含 billing:read),或自定义 billing:read;不要使用 Gradient AI 推理 Key。":"Please create an account-level Personal Access Token. Choose Read Only (api:read incl. billing:read) or a custom scope with billing:read; do not use a Gradient AI inference key.","正在检查已保存的 Token…":"Checking saved token…","已保存:":"Saved: ","(来源:":" (source: ","无法读取 Token 状态:":"Cannot read token status: ","尚未保存 DIGITALOCEAN_TOKEN。":"DIGITALOCEAN_TOKEN is not saved yet.","输入新的 dop_v1_ Token 可替换(当前值已隐藏)":"Enter a new dop_v1_ token to replace it (current value hidden)","保存中…":"Saving…","保存并查询":"Save & query","当前 Token 来自只读环境变量;请在原来源中修改,页面不会覆盖它。":"The current token comes from a read-only environment variable; edit it at its source — this panel will not overwrite it.","Token 已安全保存;页面和 API 响应不会回传明文。":"Token stored securely; neither the page nor API responses echo it back.","SiliconFlow 查询说明":"SiliconFlow query notes","公开 API 已成功返回 ¥0.00。SiliconFlow 的 /v1/user/info 当前不返回代金券或历史用量;控制台可用总额可能非零。这里忠实展示 API 原始余额字段,不把它等同于控制台完整额度。":"The public API returned ¥0.00 successfully. SiliconFlow's /v1/user/info currently returns no vouchers or historical usage; the console total may still be non-zero. The raw balance field from the API is shown faithfully here and should not be read as the full console credit.","余额卡片展示 SiliconFlow /v1/user/info 公开 API 返回的原始余额字段。":"The balance card shows the raw balance field returned by SiliconFlow's public /v1/user/info API.","返回字段":"Returned fields","凭据读取规则":"Credential lookup rules","插件只检查“设置 → 模型”中 Provider ID 或显示名为 siliconflow 的提供商,并读取其 apiKeyEnv 对应的已保存 API Key。":"The plugin only checks providers in Settings → Models whose Provider ID or display name is siliconflow, and reads the saved API key referenced by their apiKeyEnv.","如果未找到提供商、未填写 API Key 或凭据引用失效,插件会停止查询并说明需要修复的配置;不会回退到其他服务商的 Key。":"If no such provider exists, the key is empty, or the credential reference is broken, the plugin stops querying and explains what needs fixing; it never falls back to another provider's key.","DigitalOcean 查询的是主账户 Billing API;DigitalOcean AI 推理 Key 与 AMD GPU Cloud 推理 Key 均不能直接查询账单。":"DigitalOcean queries the main account Billing API; DigitalOcean AI inference keys and AMD GPU Cloud inference keys cannot query billing directly.","\n调用 ":"\ncalls ",")\n输入·未命中 ":")\ninput·miss ","\n高峰消耗 ":"\npeak cost ","\n无记录":"\nno records","查询 DeepSeek、SiliconFlow、DigitalOcean 与百炼 Token Plan":"Query DeepSeek, SiliconFlow, DigitalOcean & Bailian Token Plan balances","百炼 Token Plan · 1 周配额":"Bailian Token Plan · 1-week quota","百炼 Token Plan 配额用量":"Bailian Token Plan quota usage","百炼 Token Plan":"Bailian Token Plan","本周配额":"This week's quota","开始":"Start","结束":"End","已用 ":"Used ","百炼 Token Plan 查询说明":"Bailian Token Plan query notes","本查询复用百炼 CLI(bl)的“控制台登录”OAuth token,不需要阿里云 AccessKey,权限面很小。":"This query reuses the console-login OAuth token from the Bailian CLI (bl); no Alibaba Cloud AccessKey is needed and the permission scope is minimal.","如尚未登录,请先在终端执行:bl auth login --console,然后回到本页点“查询余额”。":"If you haven't logged in yet, run: bl auth login --console, then come back and click 'Query balance'.","打开百炼 CLI 登录说明":"Open Bailian CLI login docs","百炼控制台 OAuth 登录(~/.bailian/config.json 的 access_token,用 bl auth login --console 生成)":"Bailian console OAuth login (access_token in ~/.bailian/config.json, created by bl auth login --console)","数据来自阿里云百炼控制台内部门户网关(zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage),展示的是 1 周配额已用进度与周期。":"Data from Alibaba Cloud Bailian console portal gateway (zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage), showing 1-week quota usage progress and period.","帮助与说明":"Help & info","本月已消耗":"Spent this month","剩余 ":"left ","超支 ":"over budget by ","月度预算":"Monthly budget","当前预算:":"Current budget: ","设置每月消费上限,概览页会显示进度与超支预警。":"Set a monthly spending cap; the overview shows progress and an over-budget warning.",";概览页会按当月消耗显示进度与超支预警。":"; the overview shows monthly progress and over-budget warnings.","预算金额(元)":"Budget amount (CNY)","保存预算":"Save budget","清除预算":"Clear budget","请输入预算金额":"Please enter a budget amount","预算金额必须是非负数字":"Budget must be a non-negative number","消耗趋势(近 30 天)":"Cost trend (last 30 days)","模型消耗占比":"Model cost share","会话消耗排行":"Top conversations by cost","按模型 / 服务商 / 会话搜索…":"Search by model / provider / session…","没有符合条件的记录。":"No records match the current filters.","暂无数据":"No data","清除":"Clear","总消耗 ":"Total cost ","调用 ":"calls ","空闲 ":"off-peak "," 次":" calls","较上月":"vs last month","日均 ":"daily avg ","峰值日 ":"peak day ","天":"d","收起帮助":"Collapse help","模型消耗明细":"Model cost breakdown","按模型分组":"Grouped by model"," 个模型":" models","服务商×模型明细":"Provider × model breakdown","展开每个服务商下的模型分布":"Each provider's model breakdown","每个模型的调用次数、token 用量(输入·未命中 / 缓存命中 / 输出 / 推理)与高峰/空闲分列的消耗。合计行高亮汇总所有模型的总量。":"Per-model call count, token usage (input miss / cache hit / output / reasoning) and peak/off-peak costs. The highlighted total row sums everything above.","先按服务商(DeepSeek 官方 / SiliconFlow / DigitalOcean …)分组,组内按消耗降序列出该服务商下的每个模型及对应消耗。":"Grouped by provider (DeepSeek official / SiliconFlow / DigitalOcean …); within each group, models are listed by cost."};
25
25
 
26
26
  function __detectLang() {
27
27
  // 语言完全跟随宿主设置(通用设置 → 语言):不再读 localStorage,避免旧选择覆盖系统语言
@@ -242,7 +242,62 @@ window.__ModuleLoader__.load({
242
242
  nowPeriod: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", marginTop: 10, fontSize: fs(12) },
243
243
  loading: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, padding: "48px 0", color: "inherit" },
244
244
  spinner: { width: 28, height: 28, borderRadius: "50%", border: "3px solid rgba(90,140,255,.22)", borderTopColor: "#5a8cff", animation: "dshUsageSpin .8s linear infinite", flexShrink: 0 },
245
- loadText: { fontSize: fs(12), opacity: 0.65 }
245
+ loadText: { fontSize: fs(12), opacity: 0.65 },
246
+ // ── 新增:概览 Hero / 主卡 / 图表 / 预算 / 会话排行 / 说明抽屉 ──
247
+ heroUsage: { borderRadius: 12, padding: "14px 18px", color: "#fff", background: "linear-gradient(135deg,#3a7bd5,#00d2ff)", display: "flex", flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 14, flexWrap: "wrap", marginTop: 12 },
248
+ heroUsageLeft: { display: "flex", flexDirection: "column", gap: 3 },
249
+ heroUsageLabel: { fontSize: fs(12), opacity: 0.85 },
250
+ heroUsageValue: { fontSize: fs(26), fontWeight: 700, letterSpacing: 0.5 },
251
+ heroUsageMeta: { fontSize: fs(11), opacity: 0.9 },
252
+ heroUsageRight: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
253
+ // 预算进度条(白色高对比,放在 hero 内)
254
+ budgetHero: { flex: "1 1 220px", minWidth: 200 },
255
+ budgetHeroLabel: { fontSize: fs(11), opacity: 0.9, marginBottom: 4, display: "flex", justifyContent: "space-between", gap: 8 },
256
+ budgetHeroBar: { width: "100%", height: 10, borderRadius: 999, background: "rgba(0,0,0,.22)", overflow: "hidden", position: "relative", boxShadow: "inset 0 1px 2px rgba(0,0,0,.25)" },
257
+ budgetHeroFill: { height: "100%", borderRadius: 999, background: "linear-gradient(90deg,#ffffff,#d7ecff)", transition: "width .4s ease" },
258
+ budgetHeroFillWarn: { height: "100%", borderRadius: 999, background: "linear-gradient(90deg,#ffe08a,#ffb347)", transition: "width .4s ease" },
259
+ budgetHeroFillDanger: { height: "100%", borderRadius: 999, background: "linear-gradient(90deg,#ff9a8b,#ff6b6b)", transition: "width .4s ease" },
260
+ budgetHeroText: { fontSize: fs(10.5), opacity: 0.95, marginTop: 4 },
261
+ // 3 大主卡
262
+ bigCards: { display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(150px,1fr))", gap: 10 },
263
+ bigCard: { border: "1px solid rgba(128,128,128,.35)", borderRadius: 10, padding: "12px 14px", minWidth: 0, display: "flex", flexDirection: "column", gap: 2 },
264
+ bigCardPrimary: { border: "1px solid rgba(90,140,255,.45)", background: "rgba(90,140,255,.08)", borderRadius: 10, padding: "12px 14px", minWidth: 0, display: "flex", flexDirection: "column", gap: 2 },
265
+ bigCardL: { fontSize: fs(11), opacity: 0.6 },
266
+ bigCardV: { fontSize: fs(22), fontWeight: 700, overflowWrap: "anywhere" },
267
+ bigCardH: { fontSize: fs(10), opacity: 0.5, overflowWrap: "anywhere" },
268
+ // 图表区
269
+ chartGrid: { display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(340px,1fr))", gap: 12, marginTop: 12 },
270
+ chartPanel: { border: "1px solid rgba(128,128,128,.18)", borderRadius: 10, padding: "10px 12px", minWidth: 0 },
271
+ chartPanelTitle: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, fontSize: fs(12), fontWeight: 600, marginBottom: 6 },
272
+ chartCanvas: { width: "100%", display: "block" },
273
+ legendRow: { display: "flex", alignItems: "center", gap: 8, fontSize: fs(11), opacity: 0.8, flexWrap: "wrap", marginTop: 6 },
274
+ legendDot: { display: "inline-block", width: 10, height: 10, borderRadius: 3 },
275
+ // token 总览小条(筛选范围内的 token 汇总)
276
+ tokenBar: { display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(120px,1fr))", gap: 8, marginTop: 10 },
277
+ tokenCell: { border: "1px solid rgba(128,128,128,.14)", borderRadius: 8, padding: "6px 10px", minWidth: 0 },
278
+ tokenCellL: { fontSize: fs(10), opacity: 0.55 },
279
+ tokenCellV: { fontSize: fs(13), fontWeight: 600, overflowWrap: "anywhere" },
280
+ // 环比徽章(涨/跌)
281
+ badgeUp: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(11), fontWeight: 500, background: "rgba(231,76,60,.16)", color: "#e74c3c", whiteSpace: "nowrap" },
282
+ badgeDown: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(11), fontWeight: 500, background: "rgba(46,204,113,.18)", color: "#2ecc71", whiteSpace: "nowrap" },
283
+ // 会话排行
284
+ sessionCard: { border: "1px solid rgba(128,128,128,.18)", borderRadius: 10, padding: "10px 12px", marginTop: 12 },
285
+ sessionRow: { display: "flex", alignItems: "center", gap: 10, padding: "6px 0", borderBottom: "1px solid rgba(128,128,128,.12)", fontSize: fs(12) },
286
+ sessionName: { flex: "1 1 0", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
287
+ sessionBar: { flex: "1 1 120px", height: 8, borderRadius: 999, background: "rgba(128,128,128,.15)", overflow: "hidden", minWidth: 80 },
288
+ sessionBarFill: { height: "100%", borderRadius: 999, background: "linear-gradient(90deg,#3d6bd6,#5a8cff)" },
289
+ sessionCost: { textAlign: "right", whiteSpace: "nowrap", fontWeight: 600, minWidth: 74 },
290
+ sessionSub: { fontSize: fs(10), opacity: 0.55, whiteSpace: "nowrap" },
291
+ // 详细数据区(分组标题,更醒目 + 副标题说明)
292
+ dataSection: { marginTop: 18, paddingTop: 14, borderTop: "1px solid rgba(128,128,128,.18)" },
293
+ dataSectionTitle: { display: "flex", alignItems: "baseline", gap: 8, fontSize: fs(14), fontWeight: 700, marginBottom: 2, color: "inherit" },
294
+ dataSectionHint: { fontSize: fs(11), opacity: 0.55, marginBottom: 8 },
295
+ // 表格行:合计行更突出
296
+ tdTotal: { textAlign: "right", padding: "8px 8px", fontWeight: 700, borderTop: "2px solid rgba(90,140,255,.5)", background: "rgba(90,140,255,.06)", whiteSpace: "normal", wordBreak: "break-word" },
297
+ tdTotalFirst: { textAlign: "left", padding: "8px 8px", fontWeight: 700, borderTop: "2px solid rgba(90,140,255,.5)", background: "rgba(90,140,255,.06)", whiteSpace: "normal", wordBreak: "break-word" },
298
+ // 模型名 / API 服务商列:更宽 + 优雅换行
299
+ tdModelName: { textAlign: "left", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.18)", whiteSpace: "normal", wordBreak: "break-word", lineHeight: 1.45, minWidth: 180, maxWidth: 260 },
300
+ tdModelProvider: { textAlign: "left", padding: "7px 10px", borderBottom: "1px solid rgba(128,128,128,.18)", whiteSpace: "normal", wordBreak: "break-word", lineHeight: 1.45, minWidth: 160, maxWidth: 220, opacity: 0.85, fontSize: fs(11) }
246
301
  };
247
302
 
248
303
  // 注入全局样式(幂等,仅一次):加载动画关键帧等
@@ -264,6 +319,25 @@ window.__ModuleLoader__.load({
264
319
  }
265
320
 
266
321
  // ── helpers ──
322
+ // canvas 图表颜色适配宿主主题:读取一个隐藏 probe 元素的继承文字颜色,
323
+ // 判断当前是浅色还是深色背景,从而选择可读的文字/网格颜色。
324
+ var isLightBackground = function () {
325
+ try {
326
+ var probe = document.createElement("span");
327
+ probe.style.cssText = "position:absolute;visibility:hidden;pointer-events:none;font-size:10px";
328
+ probe.textContent = "x";
329
+ if (document.body) document.body.appendChild(probe);
330
+ var c = document.body ? window.getComputedStyle(probe).color : "";
331
+ if (document.body && probe.parentNode) probe.parentNode.removeChild(probe);
332
+ var m = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(c || "");
333
+ if (m) {
334
+ var luma = 0.299 * +m[1] + 0.587 * +m[2] + 0.114 * +m[3];
335
+ return luma < 128;
336
+ }
337
+ } catch (e) {}
338
+ try { return !(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches); } catch (e2) {}
339
+ return true;
340
+ };
267
341
  var pad2 = function (n) { return (n < 10 ? "0" : "") + n; };
268
342
  var fmtInt = function (n) { return String(Math.round(n || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, ","); };
269
343
  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()); };
@@ -656,9 +730,244 @@ var api = function (payload) {
656
730
  ctx.fillText(t("计价按请求的 API 服务商分别应用已核验价格;DigitalOcean 美元价按 USD/CNY 汇率折算;无法可靠映射的第三方价格按 ¥0。"), P, y);
657
731
  }
658
732
 
733
+ // ── 近 30 天消耗趋势图(canvas 堆叠柱状:高峰橙 + 空闲蓝)──
734
+ var TREND_COLORS = { peak: "#ff9800", off: "#5a8cff" };
735
+ function TrendChart(props) {
736
+ var days = props.days || [];
737
+ var canvasRef = React.useRef(null);
738
+ var wrapRef = React.useRef(null);
739
+ var tipState = React.useState(null);
740
+ var tip = tipState[0], setTip = tipState[1];
741
+ var rangeState = React.useState(props.range || 30);
742
+ var range = rangeState[0], setRange = rangeState[1];
743
+ // 组装最近 N 天(含无记录日),时间正序
744
+ function buildSeries() {
745
+ var N = range, map = {}, out = [];
746
+ for (var i = 0; i < days.length; i++) map[days[i].day] = days[i];
747
+ var nowKey = bjKey(Date.now());
748
+ var p = nowKey.split("-");
749
+ var base = Date.UTC(+p[0], +p[1] - 1, +p[2]) - 8 * 3600 * 1000;
750
+ for (var k = N - 1; k >= 0; k--) {
751
+ var ms = base - k * 86400000;
752
+ var d = new Date(ms + 8 * 3600 * 1000);
753
+ var key = d.getUTCFullYear() + "-" + pad2(d.getUTCMonth() + 1) + "-" + pad2(d.getUTCDate());
754
+ var rec = map[key];
755
+ var split = daySplit(rec, props.regime || "auto");
756
+ out.push({ key: key, peak: split.peak, off: split.off, total: split.peak + split.off, calls: rec ? (rec.calls || 0) : 0 });
757
+ }
758
+ return out;
759
+ }
760
+ function redraw() {
761
+ var node = canvasRef.current, wrap = wrapRef.current;
762
+ if (!node || !wrap) return;
763
+ var series = buildSeries();
764
+ var cssW = wrap.clientWidth || 600, cssH = 190;
765
+ var dpr = (typeof window !== "undefined" && window.devicePixelRatio) || 1;
766
+ node.width = Math.round(cssW * dpr);
767
+ node.height = Math.round(cssH * dpr);
768
+ node.style.width = cssW + "px";
769
+ node.style.height = cssH + "px";
770
+ var ctx = node.getContext("2d");
771
+ if (!ctx) return;
772
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
773
+ ctx.clearRect(0, 0, cssW, cssH);
774
+ var lightBg = isLightBackground();
775
+ var inkMuted = lightBg ? "rgba(128,128,128,.6)" : "rgba(215,222,232,.6)";
776
+ var gridColor = lightBg ? "rgba(128,128,128,.15)" : "rgba(255,255,255,.15)";
777
+ var emptyColor = lightBg ? "rgba(128,128,128,.5)" : "rgba(215,222,232,.55)";
778
+ var padL = 54, padR = 10, padT = 18, padB = 24;
779
+ var chartW = cssW - padL - padR, chartH = cssH - padT - padB;
780
+ var maxV = 0;
781
+ for (var i = 0; i < series.length; i++) if (series[i].total > maxV) maxV = series[i].total;
782
+ if (maxV <= 0) {
783
+ ctx.fillStyle = emptyColor;
784
+ ctx.font = "12px sans-serif";
785
+ ctx.textAlign = "center";
786
+ ctx.fillText(t("暂无数据"), cssW / 2, cssH / 2);
787
+ node.__series = null;
788
+ return;
789
+ }
790
+ ctx.strokeStyle = gridColor;
791
+ ctx.lineWidth = 1;
792
+ ctx.fillStyle = inkMuted;
793
+ ctx.font = "10px sans-serif";
794
+ ctx.textAlign = "right";
795
+ for (var g = 0; g <= 4; g++) {
796
+ var gy = padT + chartH - (chartH * g / 4);
797
+ ctx.beginPath(); ctx.moveTo(padL, gy); ctx.lineTo(cssW - padR, gy); ctx.stroke();
798
+ ctx.fillText(fmtMoney(maxV * g / 4).replace("¥", ""), padL - 6, gy + 3);
799
+ }
800
+ var bw = Math.max(1, Math.min(14, (chartW - 6) / series.length - 2));
801
+ var gap = bw + 2, totalW = gap * series.length - 2, x0 = padL + (chartW - totalW) / 2;
802
+ var labelStep = Math.max(1, Math.ceil(series.length / 8));
803
+ for (var s = 0; s < series.length; s++) {
804
+ var it = series[s], x = x0 + s * gap;
805
+ var offH = it.off > 0 ? (it.off / maxV * chartH) : 0;
806
+ var peakH = it.peak > 0 ? (it.peak / maxV * chartH) : 0;
807
+ var yBottom = padT + chartH;
808
+ if (offH > 0) { ctx.fillStyle = TREND_COLORS.off; ctx.fillRect(x, yBottom - offH, bw, offH); }
809
+ if (peakH > 0) { ctx.fillStyle = TREND_COLORS.peak; ctx.fillRect(x, yBottom - offH - peakH, bw, peakH); }
810
+ if (s % labelStep === 0 || s === series.length - 1) {
811
+ ctx.fillStyle = inkMuted;
812
+ ctx.textAlign = "center";
813
+ var dp = it.key.split("-");
814
+ ctx.fillText((+dp[1]) + "/" + (+dp[2]), x + bw / 2, cssH - 6);
815
+ }
816
+ }
817
+ node.__series = series; node.__x0 = x0; node.__gap = gap; node.__bw = bw; node.__padT = padT; node.__chartH = chartH;
818
+ }
819
+ React.useEffect(function () {
820
+ redraw();
821
+ function onResize() { redraw(); }
822
+ if (typeof window !== "undefined") window.addEventListener("resize", onResize);
823
+ return function () { if (typeof window !== "undefined") window.removeEventListener("resize", onResize); };
824
+ }, [days, props.regime, range]);
825
+ function onMove(e) {
826
+ var node = canvasRef.current;
827
+ if (!node || !node.__series) return;
828
+ var rect = node.getBoundingClientRect();
829
+ var idx = Math.floor((e.clientX - rect.left - node.__x0) / node.__gap);
830
+ if (idx < 0 || idx >= node.__series.length) { setTip(null); return; }
831
+ var it = node.__series[idx];
832
+ setTip({ left: node.__x0 + idx * node.__gap + node.__bw / 2, text: dayLabel(it.key) + "\n" + t("总消耗 ") + fmtMoney(it.total) + "(" + t("高峰 ") + fmtMoney(it.peak) + " · " + t("空闲 ") + fmtMoney(it.off) + ")\n" + t("调用 ") + it.calls + t(" 次") });
833
+ }
834
+ return el("div", { style: { minWidth: 0 } },
835
+ el("div", { style: st.calBar, marginTop: 0 },
836
+ el("div", { style: st.seg },
837
+ [7, 30, 90].map(function (n) {
838
+ return el("button", { key: n, style: range === n ? st.segBtnOn : st.segBtn, onClick: function () { setRange(n); } }, n + t("天"));
839
+ })
840
+ )
841
+ ),
842
+ el("div", { style: { position: "relative", minWidth: 0 }, ref: wrapRef },
843
+ el("canvas", { ref: canvasRef, style: st.chartCanvas, onMouseMove: onMove, onMouseLeave: function () { setTip(null); } }),
844
+ tip ? el("div", { style: { position: "absolute", top: 4, left: Math.max(0, Math.min(tip.left - 90, (wrapRef.current ? wrapRef.current.clientWidth - 220 : 220))), background: "rgba(20,24,32,.92)", color: "#fff", borderRadius: 6, padding: "6px 10px", fontSize: fs(11), lineHeight: 1.6, whiteSpace: "pre", pointerEvents: "none", zIndex: 5, maxWidth: "92%" } }, tip.text) : null
845
+ )
846
+ );
847
+ }
848
+
849
+ // ── 模型消耗占比环形图(canvas 环形 + HTML 图例)──
850
+ var DONUT_COLORS = ["#5a8cff", "#ff9800", "#22a45d", "#e74c3c", "#9b59b6", "#00d2ff", "#e84393", "#fdcb6e", "#6c5ce7", "#00b894"];
851
+ function DonutChart(props) {
852
+ var rows = props.rows || [];
853
+ var canvasRef = React.useRef(null);
854
+ var modeState = React.useState("cost");
855
+ var mode = modeState[0], setMode = modeState[1];
856
+ function totalVal() { var s = 0; for (var i = 0; i < rows.length; i++) s += (mode === "calls" ? (rows[i].calls || 0) : rows[i].cost); return s; }
857
+ function draw() {
858
+ var node = canvasRef.current;
859
+ if (!node) return;
860
+ var total = totalVal();
861
+ var cssW = 176, cssH = 176;
862
+ var dpr = (typeof window !== "undefined" && window.devicePixelRatio) || 1;
863
+ node.width = Math.round(cssW * dpr);
864
+ node.height = Math.round(cssH * dpr);
865
+ node.style.width = cssW + "px";
866
+ node.style.height = cssH + "px";
867
+ var ctx = node.getContext("2d");
868
+ if (!ctx) return;
869
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
870
+ ctx.clearRect(0, 0, cssW, cssH);
871
+ var cx = cssW / 2, cy = cssH / 2, r = 68, inner = 44;
872
+ var start = -Math.PI / 2;
873
+ for (var i2 = 0; i2 < rows.length; i2++) {
874
+ var v = mode === "calls" ? (rows[i2].calls || 0) : rows[i2].cost;
875
+ var slice = total > 0 ? (v / total * Math.PI * 2) : (2 * Math.PI / Math.max(rows.length, 1));
876
+ ctx.beginPath();
877
+ ctx.arc(cx, cy, r, start, start + slice);
878
+ ctx.arc(cx, cy, inner, start + slice, start, true);
879
+ ctx.closePath();
880
+ ctx.fillStyle = DONUT_COLORS[i2 % DONUT_COLORS.length];
881
+ ctx.fill();
882
+ start += slice;
883
+ }
884
+ var lightBg = isLightBackground();
885
+ var inkMain = lightBg ? "#1c2733" : "#e6ebf2";
886
+ var inkMuted = lightBg ? "rgba(28,39,51,.6)" : "rgba(225,231,240,.6)";
887
+ if (total <= 0) {
888
+ ctx.strokeStyle = lightBg ? "rgba(128,128,128,.2)" : "rgba(255,255,255,.22)";
889
+ ctx.lineWidth = 2;
890
+ ctx.beginPath(); ctx.arc(cx, cy, (r + inner) / 2, 0, Math.PI * 2); ctx.stroke();
891
+ }
892
+ ctx.fillStyle = inkMain; ctx.font = "700 15px sans-serif"; ctx.textAlign = "center";
893
+ ctx.fillText(total > 0 ? (mode === "calls" ? fmtInt(total) : fmtMoney(total)) : "—", cx, cy - 2);
894
+ ctx.fillStyle = inkMuted; ctx.font = "10px sans-serif";
895
+ ctx.fillText(mode === "calls" ? t("调用次数") : t("总消耗"), cx, cy + 13);
896
+ }
897
+ React.useEffect(function () {
898
+ draw();
899
+ function onResize() { draw(); }
900
+ if (typeof window !== "undefined") window.addEventListener("resize", onResize);
901
+ return function () { if (typeof window !== "undefined") window.removeEventListener("resize", onResize); };
902
+ }, [rows, mode]);
903
+ var tot = totalVal();
904
+ return el("div", { style: { minWidth: 0 } },
905
+ el("div", { style: Object.assign({}, st.calBar, { marginTop: 0, justifyContent: "center" }) },
906
+ el("div", { style: st.seg },
907
+ el("button", { style: mode === "cost" ? st.segBtnOn : st.segBtn, onClick: function () { setMode("cost"); } }, t("按消耗")),
908
+ el("button", { style: mode === "calls" ? st.segBtnOn : st.segBtn, onClick: function () { setMode("calls"); } }, t("按调用数"))
909
+ )
910
+ ),
911
+ el("div", { style: { display: "flex", justifyContent: "center" } },
912
+ el("canvas", { ref: canvasRef, style: { width: 176, height: 176, display: "block" } })
913
+ ),
914
+ rows.length ? el("div", { style: st.legendRow },
915
+ rows.slice(0, 10).map(function (r, idx) {
916
+ var v = mode === "calls" ? (r.calls || 0) : r.cost;
917
+ var p = tot > 0 ? Math.round(v / tot * 100) : 0;
918
+ var nm = r.name || r.key || "—";
919
+ return el("span", { key: "d-" + idx, title: nm, style: { display: "inline-flex", alignItems: "center", gap: 5, maxWidth: "100%" } },
920
+ el("span", { style: Object.assign({}, st.legendDot, { background: DONUT_COLORS[idx % DONUT_COLORS.length] }) }),
921
+ el("span", { style: { overflowWrap: "anywhere" } }, nm + " " + p + "%")
922
+ );
923
+ }),
924
+ rows.length > 10 ? el("span", null, "…") : null
925
+ ) : null
926
+ );
927
+ }
928
+
929
+ // ── 会话消耗排行 Top 10 ──
930
+ function SessionRank(props) {
931
+ var records = props.records || [], regime = props.regime || "auto";
932
+ var bySid = {};
933
+ for (var si = 0; si < records.length; si++) {
934
+ var r = records[si];
935
+ var sid = String(r.sessionId || "").trim();
936
+ if (!sid) continue;
937
+ var c = costOf(r, regime);
938
+ var g = bySid[sid];
939
+ if (!g) { g = bySid[sid] = { sid: sid, cost: 0, calls: 0, last: 0, tokens: 0 }; }
940
+ g.cost += c; g.calls += 1; if (r.time > g.last) g.last = r.time;
941
+ g.tokens += (r.inputTokens || 0) + (r.outputTokens || 0) + (r.cacheReadTokens || 0) + (r.cacheWriteTokens || 0) + (r.reasoningTokens || 0);
942
+ }
943
+ var rows = [];
944
+ for (var k in bySid) rows.push(bySid[k]);
945
+ rows.sort(function (a, b) { return b.cost - a.cost || b.last - a.last; });
946
+ rows = rows.slice(0, 10);
947
+ if (rows.length === 0) return null;
948
+ var maxCost = rows[0].cost;
949
+ return el("div", { style: st.sessionCard },
950
+ el("div", { style: st.chartPanelTitle }, t("会话消耗排行") + " · Top " + rows.length),
951
+ rows.map(function (row, idx) {
952
+ var pctv = maxCost > 0 ? Math.round(row.cost / maxCost * 100) : 0;
953
+ var label = row.sid;
954
+ if (label.length > 26) label = label.slice(0, 26) + "…";
955
+ return el("div", { key: row.sid, style: st.sessionRow },
956
+ el("span", { style: st.sessionName, title: row.sid }, (idx + 1) + ". " + label),
957
+ el("div", { style: st.sessionBar },
958
+ el("div", { style: Object.assign({}, st.sessionBarFill, { width: pctv + "%" }) })
959
+ ),
960
+ el("span", { style: st.sessionSub, title: t("总 token") + ": " + fmtInt(row.tokens) }, fmtInt(row.calls) + t(" 次 · ") + fmtInt(row.tokens) + " tok"),
961
+ el("span", { style: st.sessionCost }, fmtMoney(row.cost))
962
+ );
963
+ })
964
+ );
965
+ }
966
+
659
967
  // ── Overview view ──
660
968
  function OverviewView(props) {
661
969
  var allRecords = props.records, regime = props.regime;
970
+ var days = props.days || [], budget = props.budget || null;
662
971
  var toolCalls = props.toolCalls || [];
663
972
  var toolOpenState = React.useState(false);
664
973
  var toolOpen = toolOpenState[0], setToolOpen = toolOpenState[1];
@@ -669,6 +978,42 @@ var api = function (payload) {
669
978
  var toState = React.useState("");
670
979
  var toDate = toState[0], setToDate = toState[1];
671
980
 
981
+ var nowPeriod = periodNow(Date.now());
982
+ // 本月(北京时间自然月)已消耗 / 调用 / 日均 / 峰值日:不受概览日期筛选影响
983
+ var monthCost = 0, monthCalls = 0, monthDayMap = {};
984
+ var nowB = new Date(Date.now() + 8 * 3600 * 1000);
985
+ var ymPrefix = nowB.getUTCFullYear() + "-" + pad2(nowB.getUTCMonth() + 1);
986
+ var monthStartMs = Date.UTC(nowB.getUTCFullYear(), nowB.getUTCMonth(), 1) - 8 * 3600 * 1000;
987
+ var prevMonthStartMs = Date.UTC(nowB.getUTCFullYear(), nowB.getUTCMonth() - 1, 1) - 8 * 3600 * 1000;
988
+ var prevMonthCost = 0;
989
+ for (var mi = 0; mi < allRecords.length; mi++) {
990
+ var mr = allRecords[mi];
991
+ var mk2 = bjKey(mr.time);
992
+ if (mk2.indexOf(ymPrefix) === 0) {
993
+ var mc = costOf(mr, "auto");
994
+ monthCost += mc;
995
+ monthCalls += 1;
996
+ if (!monthDayMap[mk2]) monthDayMap[mk2] = 0;
997
+ monthDayMap[mk2] += mc;
998
+ } else if (mr.time >= prevMonthStartMs && mr.time < monthStartMs) {
999
+ prevMonthCost += costOf(mr, "auto");
1000
+ }
1001
+ }
1002
+ var moDelta = prevMonthCost > 0 ? (monthCost - prevMonthCost) / prevMonthCost * 100 : null;
1003
+ var peakDayKey = "", peakDayCost = 0;
1004
+ for (var pdk in monthDayMap) { if (monthDayMap[pdk] > peakDayCost) { peakDayCost = monthDayMap[pdk]; peakDayKey = pdk; } }
1005
+ var monthAvgCost = nowB.getUTCDate() > 0 ? monthCost / nowB.getUTCDate() : 0;
1006
+ var budgetPct = budget && budget.monthly > 0 ? Math.min(100, monthCost / budget.monthly * 100) : 0;
1007
+ function budgetStyle() {
1008
+ if (budgetPct >= 100) return st.budgetHeroFillDanger;
1009
+ if (budgetPct >= 80) return st.budgetHeroFillWarn;
1010
+ return st.budgetHeroFill;
1011
+ }
1012
+ function budgetText() {
1013
+ if (budgetPct >= 100) return t("超支 ") + fmtMoney(monthCost - budget.monthly);
1014
+ return t("已用 ") + budgetPct.toFixed(0) + "% · " + t("剩余 ") + fmtMoney(budget.monthly - monthCost);
1015
+ }
1016
+
672
1017
  var nowKey = bjKey(Date.now());
673
1018
  var fromMs = null, toMs = null;
674
1019
  if (preset === "today") { fromMs = bjStartMs(nowKey); toMs = Date.now(); }
@@ -723,9 +1068,20 @@ var api = function (payload) {
723
1068
  }
724
1069
  }
725
1070
  var hitRate = (totalHit + totalMiss + totalWrite) > 0 ? (totalHit / (totalHit + totalMiss + totalWrite) * 100).toFixed(1) + "%" : "—";
1071
+ var tokenTotals = [
1072
+ { l: t("总 token"), v: fmtInt(totalMiss + totalHit + totalWrite + totalOut + totalReason) },
1073
+ { l: t("输入 · 未命中"), v: fmtInt(totalMiss) },
1074
+ { l: t("缓存命中"), v: fmtInt(totalHit) },
1075
+ { l: t("输出"), v: fmtInt(totalOut) },
1076
+ { l: t("推理"), v: totalReason ? fmtInt(totalReason) : "—" }
1077
+ ];
726
1078
  var modelRows = [];
727
1079
  for (var k in byModel) modelRows.push(byModel[k]);
728
1080
  modelRows.sort(function (a, b) { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
1081
+ // 环形图数据:按消耗降序
1082
+ var donutRows = [];
1083
+ for (var dr = 0; dr < modelRows.length; dr++) donutRows.push({ key: modelRows[dr].key, name: modelRows[dr].name, cost: modelRows[dr].cost, calls: modelRows[dr].calls });
1084
+ donutRows.sort(function (a, b) { return b.cost - a.cost || (a.key < b.key ? -1 : 1); });
729
1085
  for (var mr = 0; mr < modelRows.length; mr++) {
730
1086
  var pset = {};
731
1087
  for (var rr = 0; rr < records.length; rr++) {
@@ -828,56 +1184,125 @@ var api = function (payload) {
828
1184
  el("div", { style: st.note, marginTop: 8 },
829
1185
  t("当前范围:共 ") + fmtInt(records.length) + t(" 条") + (preset === "all" && !fromDate && !toDate ? t("(全部记录)") : "") + t(" · 总消耗 ") + fmtMoney(totalCost) + "(" + costHint(regime) + ")"
830
1186
  ),
1187
+ el("div", { style: st.heroUsage },
1188
+ el("div", { style: st.heroUsageLeft },
1189
+ el("div", { style: st.heroUsageLabel }, t("本月已消耗") + (monthCalls ? " · " + fmtInt(monthCalls) + t(" 次") : "")),
1190
+ el("div", { style: st.heroUsageValue }, fmtMoney(monthCost)),
1191
+ el("div", { style: st.heroUsageMeta },
1192
+ el("span", null, t("当前 · ") + t(nowPeriod.label)),
1193
+ monthCalls ? el("span", { style: { marginLeft: 6, opacity: 0.9 } }, " · " + t("日均 ") + fmtMoney(monthAvgCost)) : null,
1194
+ moDelta != null ? el("span", { style: moDelta > 0 ? st.badgeUp : st.badgeDown, marginLeft: 6 }, (moDelta > 0 ? "▲ " : "▼ ") + Math.abs(moDelta).toFixed(1) + "% " + t("较上月")) : null,
1195
+ peakDayKey ? el("span", { style: { marginLeft: 6, opacity: 0.9 } }, " · " + t("峰值日 ") + dayLabel(peakDayKey) + " " + fmtMoney(peakDayCost)) : null
1196
+ )
1197
+ ),
1198
+ budget && budget.monthly > 0
1199
+ ? el("div", { style: st.budgetHero },
1200
+ el("div", { style: st.budgetHeroLabel },
1201
+ el("span", null, t("月度预算")),
1202
+ el("span", null, fmtMoney(monthCost) + " / " + fmtMoney(budget.monthly))
1203
+ ),
1204
+ el("div", { style: st.budgetHeroBar },
1205
+ el("div", { style: Object.assign({}, budgetStyle(), { width: Math.max(budgetPct > 0 ? 4 : 0, budgetPct) + "%" }) })
1206
+ ),
1207
+ el("div", { style: st.budgetHeroText }, budgetText())
1208
+ )
1209
+ : el("div", { style: st.heroUsageRight },
1210
+ el("span", { style: nowPeriod.peak ? st.badgePeak : st.badgeValley }, t("当前 · ") + t(nowPeriod.label))
1211
+ )
1212
+ ),
1213
+ el("div", { style: st.bigCards },
1214
+ el("div", { style: st.bigCardPrimary },
1215
+ el("div", { style: st.bigCardL }, t("总消耗")),
1216
+ el("div", { style: st.bigCardV }, fmtMoney(totalCost)),
1217
+ el("div", { style: st.bigCardH }, t("高峰 + 空闲 · ") + regimeLabel(regime))
1218
+ ),
1219
+ el("div", { style: st.bigCard },
1220
+ el("div", { style: st.bigCardL }, t("调用次数")),
1221
+ el("div", { style: st.bigCardV }, fmtInt(totalCalls)),
1222
+ el("div", { style: st.bigCardH }, t("高峰 ") + fmtInt(peakCalls) + t(" · 空闲 ") + fmtInt(offCalls) + (totalInterrupted > 0 ? " · " + t("中断 ") + fmtInt(totalInterrupted) + t("(未计费)") : ""))
1223
+ ),
1224
+ el("div", { style: st.bigCard },
1225
+ el("div", { style: st.bigCardL }, t("缓存命中率")),
1226
+ el("div", { style: st.bigCardV }, hitRate),
1227
+ el("div", { style: st.bigCardH }, t("输入 · 缓存命中 ") + fmtInt(totalHit))
1228
+ )
1229
+ ),
831
1230
  el("div", { style: st.cards },
832
- el(Card, { label: t("调用次数"), value: fmtInt(totalCalls), hint: t("高峰 ") + fmtInt(peakCalls) + t(" · 空闲 ") + fmtInt(offCalls) + (totalInterrupted > 0 ? t(" · 中断 ") + fmtInt(totalInterrupted) + t("(未计费)") : "") }),
833
1231
  el(Card, { label: t("输入 · 未命中"), value: fmtInt(totalMiss), hint: "token" }),
834
- el(Card, { label: t("输入 · 缓存命中"), value: fmtInt(totalHit), hint: t("命中率 ") + hitRate }),
835
1232
  el(Card, { label: t("输出"), value: fmtInt(totalOut), hint: "token" }),
836
1233
  el(Card, { label: t("高峰消耗"), value: fmtMoney(peakTotal), hint: t("工作日高峰 9:00–12:00、14:00–18:00 · 周末全天空闲价") }),
837
- el(Card, { label: t("空闲消耗"), value: fmtMoney(offTotal), hint: t("其余空闲时段") }),
838
- el(Card, { label: t("总消耗"), value: fmtMoney(totalCost), hint: t("高峰 + 空闲 · ") + regimeLabel(regime) })
1234
+ el(Card, { label: t("空闲消耗"), value: fmtMoney(offTotal), hint: t("其余空闲时段") })
839
1235
  ),
840
- el("div", { style: st.sec }, t("消耗表(按模型)")),
841
- el("div", { style: st.scroll },
842
- el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
843
- el("thead", null, el("tr", null,
844
- el("th", { style: st.thFirst }, t("模型")), el("th", { style: st.thFirst }, t("API 服务商")), el("th", { style: st.th }, t("调用")), el("th", { style: st.th }, t("高峰/空闲")),
845
- el("th", { style: st.th }, t("输入·未命中")), el("th", { style: st.th }, t("缓存命中")), el("th", { style: st.th }, t("输出")), el("th", { style: st.th }, t("推理")),
846
- el("th", { style: st.th }, t("高峰消耗")), el("th", { style: st.th }, t("空闲消耗")), el("th", { style: st.th }, t("总消耗"))
847
- )),
848
- el("tbody", null,
849
- modelRows.map(function (m) {
850
- return el("tr", { key: m.key },
851
- el("td", { style: st.tdWrap }, m.name),
852
- el("td", { style: st.tdWrap }, m.providerText),
853
- el("td", { style: st.td }, fmtInt(m.calls)),
854
- el("td", { style: st.td }, el("span", { style: st.badgePeak }, fmtInt(m.peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(m.offCalls))),
855
- el("td", { style: st.td }, fmtInt(m.miss)),
856
- el("td", { style: st.td }, el("span", { style: st.badgeHit }, fmtInt(m.hit))),
857
- el("td", { style: st.td }, fmtInt(m.out)),
858
- el("td", { style: st.td }, m.reason ? fmtInt(m.reason) : "—"),
859
- el("td", { style: st.td }, fmtMoney(m.peakCost)),
860
- el("td", { style: st.td }, fmtMoney(m.offCost)),
861
- el("td", { style: st.td }, fmtMoney(m.cost))
862
- );
863
- }),
864
- el("tr", null,
865
- el("td", { style: st.tdTotalFirst }, t("合计")),
866
- el("td", { style: st.tdTotalFirst }, fmtInt(providerRows.length) + t(" 个服务商")),
867
- el("td", { style: st.tdTotal }, fmtInt(totalCalls)),
868
- el("td", { style: st.tdTotal }, el("span", { style: st.badgePeak }, fmtInt(peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(offCalls))),
869
- el("td", { style: st.tdTotal }, fmtInt(totalMiss)),
870
- el("td", { style: st.tdTotal }, fmtInt(totalHit)),
871
- el("td", { style: st.tdTotal }, fmtInt(totalOut)),
872
- el("td", { style: st.tdTotal }, totalReason ? fmtInt(totalReason) : "—"),
873
- el("td", { style: st.tdTotal }, fmtMoney(peakTotal)),
874
- el("td", { style: st.tdTotal }, fmtMoney(offTotal)),
875
- el("td", { style: st.tdTotal }, fmtMoney(totalCost))
1236
+ el("div", { style: st.tokenBar },
1237
+ tokenTotals.map(function (tt, tti) {
1238
+ return el("div", { key: "tt-" + tti, style: st.tokenCell },
1239
+ el("div", { style: st.tokenCellL }, tt.l),
1240
+ el("div", { style: st.tokenCellV }, tt.v)
1241
+ );
1242
+ })
1243
+ ),
1244
+ el("div", { style: st.chartGrid },
1245
+ el("div", { style: st.chartPanel },
1246
+ el("div", { style: st.chartPanelTitle }, t("消耗趋势(近 30 天)")),
1247
+ el(TrendChart, { days: days, regime: regime })
1248
+ ),
1249
+ el("div", { style: st.chartPanel },
1250
+ el("div", { style: st.chartPanelTitle }, t("模型消耗占比")),
1251
+ el(DonutChart, { rows: donutRows })
1252
+ )
1253
+ ),
1254
+ el("div", { style: st.dataSection },
1255
+ el("div", { style: st.dataSectionTitle },
1256
+ el("span", null, t("模型消耗明细")),
1257
+ el("span", { style: { fontSize: fs(11), fontWeight: 400, opacity: 0.55 } }, "· " + t("按模型分组") + " · " + fmtInt(modelRows.length) + t(" 个模型"))
1258
+ ),
1259
+ el("div", { style: st.dataSectionHint }, t("每个模型的调用次数、token 用量(输入·未命中 / 缓存命中 / 输出 / 推理)与高峰/空闲分列的消耗。合计行高亮汇总所有模型的总量。")),
1260
+ el("div", { style: st.scroll },
1261
+ el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
1262
+ el("thead", null, el("tr", null,
1263
+ el("th", { style: st.thFirst }, t("模型")), el("th", { style: st.thFirst }, t("API 服务商")), el("th", { style: st.th }, t("调用")), el("th", { style: st.th }, t("高峰/空闲")),
1264
+ el("th", { style: st.th }, t("输入·未命中")), el("th", { style: st.th }, t("缓存命中")), el("th", { style: st.th }, t("输出")), el("th", { style: st.th }, t("推理")),
1265
+ el("th", { style: st.th }, t("高峰消耗")), el("th", { style: st.th }, t("空闲消耗")), el("th", { style: st.th }, t("总消耗"))
1266
+ )),
1267
+ el("tbody", null,
1268
+ modelRows.map(function (m) {
1269
+ return el("tr", { key: m.key },
1270
+ el("td", { style: st.tdModelName, title: m.name }, m.name),
1271
+ el("td", { style: st.tdModelProvider, title: m.providerText }, m.providerText),
1272
+ el("td", { style: st.td }, fmtInt(m.calls)),
1273
+ el("td", { style: st.td }, el("span", { style: st.badgePeak }, fmtInt(m.peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(m.offCalls))),
1274
+ el("td", { style: st.td }, fmtInt(m.miss)),
1275
+ el("td", { style: st.td }, el("span", { style: st.badgeHit }, fmtInt(m.hit))),
1276
+ el("td", { style: st.td }, fmtInt(m.out)),
1277
+ el("td", { style: st.td }, m.reason ? fmtInt(m.reason) : "—"),
1278
+ el("td", { style: st.td }, fmtMoney(m.peakCost)),
1279
+ el("td", { style: st.td }, fmtMoney(m.offCost)),
1280
+ el("td", { style: Object.assign({}, st.td, { fontWeight: 600, color: "#3a7bd5" }) }, fmtMoney(m.cost))
1281
+ );
1282
+ }),
1283
+ el("tr", null,
1284
+ el("td", { style: st.tdTotalFirst }, t("合计")),
1285
+ el("td", { style: st.tdTotalFirst }, fmtInt(providerRows.length) + t(" 个服务商")),
1286
+ el("td", { style: st.tdTotal }, fmtInt(totalCalls)),
1287
+ el("td", { style: st.tdTotal }, el("span", { style: st.badgePeak }, fmtInt(peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(offCalls))),
1288
+ el("td", { style: st.tdTotal }, fmtInt(totalMiss)),
1289
+ el("td", { style: st.tdTotal }, fmtInt(totalHit)),
1290
+ el("td", { style: st.tdTotal }, fmtInt(totalOut)),
1291
+ el("td", { style: st.tdTotal }, totalReason ? fmtInt(totalReason) : "—"),
1292
+ el("td", { style: st.tdTotal }, fmtMoney(peakTotal)),
1293
+ el("td", { style: st.tdTotal }, fmtMoney(offTotal)),
1294
+ el("td", { style: st.tdTotal }, fmtMoney(totalCost))
1295
+ )
876
1296
  )
877
1297
  )
878
1298
  )
879
1299
  ),
880
- el("div", { style: st.sec, marginTop: 14 }, t("消耗明细(按 API 服务商 × 模型)")),
1300
+ el("div", { style: st.dataSection },
1301
+ el("div", { style: st.dataSectionTitle },
1302
+ el("span", null, t("服务商×模型明细")),
1303
+ el("span", { style: { fontSize: fs(11), fontWeight: 400, opacity: 0.55 } }, "· " + t("展开每个服务商下的模型分布"))
1304
+ ),
1305
+ el("div", { style: st.dataSectionHint }, t("先按服务商(DeepSeek 官方 / SiliconFlow / DigitalOcean …)分组,组内按消耗降序列出该服务商下的每个模型及对应消耗。")),
881
1306
  el("div", { style: st.scroll },
882
1307
  el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
883
1308
  el("thead", null, el("tr", null,
@@ -887,7 +1312,9 @@ var api = function (payload) {
887
1312
  )),
888
1313
  el("tbody", null, pmRows)
889
1314
  )
1315
+ )
890
1316
  ),
1317
+ el(SessionRank, { records: records, regime: regime }),
891
1318
  el("div", { style: st.note, marginTop: 8 },
892
1319
  t("DeepSeek 官方请求按官方峰谷价;SiliconFlow 按自身人民币公开价;DigitalOcean 按美元公开价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。模型名与 API 服务商均以请求参数为准。")),
893
1320
  toolCalls.length ? el("div", { style: st.sec, marginTop: 14 },
@@ -926,6 +1353,31 @@ var api = function (payload) {
926
1353
  var selectedDay = selState[0], setSelectedDay = selState[1];
927
1354
  var dimState = React.useState("cost");
928
1355
  var dimMode = dimState[0], setDim = dimState[1];
1356
+ var presetState = React.useState("all");
1357
+ var preset = presetState[0], setPreset = presetState[1];
1358
+ var fromState = React.useState("");
1359
+ var fromDate = fromState[0], setFromDate = fromState[1];
1360
+ var toState = React.useState("");
1361
+ var toDate = toState[0], setToDate = toState[1];
1362
+
1363
+ // 日期范围筛选:预设(今天/近7天/近30天/全部)+ 自定义起止,影响每日统计表与顶部卡片。
1364
+ var nowKey = bjKey(Date.now());
1365
+ var fromMs = null, toMs = null;
1366
+ if (preset === "today") { fromMs = bjStartMs(nowKey); toMs = Date.now(); }
1367
+ else if (preset === "7d") { fromMs = bjStartMs(nowKey) - 6 * 86400000; toMs = Date.now(); }
1368
+ else if (preset === "30d") { fromMs = bjStartMs(nowKey) - 29 * 86400000; toMs = Date.now(); }
1369
+ else if (preset === "custom") {
1370
+ if (fromDate) fromMs = bjStartMs(fromDate);
1371
+ if (toDate) toMs = bjStartMs(toDate) + 86400000 - 1;
1372
+ }
1373
+ var rangeActive = preset !== "all" || fromDate || toDate;
1374
+ function inRangeKey(key) {
1375
+ if (!rangeActive) return true;
1376
+ var ms = bjStartMs(key);
1377
+ if (fromMs != null && ms < fromMs) return false;
1378
+ if (toMs != null && ms > toMs) return false;
1379
+ return true;
1380
+ }
929
1381
 
930
1382
  var y = ym.y, m = ym.m;
931
1383
  var nDays = monthDays(y, m);
@@ -950,9 +1402,10 @@ var api = function (payload) {
950
1402
  for (var d2 = 1; d2 <= nDays; d2++) {
951
1403
  var key2 = y + "-" + pad2(m + 1) + "-" + pad2(d2);
952
1404
  var r2 = dayMap[key2];
953
- var v2 = r2 ? dayVal(r2) : 0;
1405
+ var inR2 = inRangeKey(key2);
1406
+ var v2 = r2 && inR2 ? dayVal(r2) : 0;
954
1407
  if (v2 > maxV) maxV = v2;
955
- if (r2) {
1408
+ if (r2 && inR2) {
956
1409
  monthCalls += r2.calls; monthMiss += r2.miss; monthHit += r2.hit; monthOut += r2.out;
957
1410
  monthPeakCalls += r2.peakCalls || 0; monthOffCalls += r2.offPeakCalls || 0;
958
1411
  var ds2 = daySplit(r2, regime, splitMap[key2]);
@@ -967,11 +1420,12 @@ var api = function (payload) {
967
1420
  for (var d3 = 1; d3 <= nDays; d3++) {
968
1421
  var key3 = y + "-" + pad2(m + 1) + "-" + pad2(d3);
969
1422
  var r3 = dayMap[key3];
970
- var v3 = r3 ? dayVal(r3) : 0;
1423
+ var inR3 = inRangeKey(key3);
1424
+ var v3 = r3 && inR3 ? dayVal(r3) : 0;
971
1425
  var inten = maxV > 0 ? v3 / maxV : 0;
972
- var bg = r3 && v3 > 0 ? "rgba(46,134,222," + (0.12 + 0.66 * inten).toFixed(2) + ")" : "rgba(128,128,128,.06)";
1426
+ var bg = r3 && inR3 && v3 > 0 ? "rgba(46,134,222," + (0.12 + 0.66 * inten).toFixed(2) + ")" : "rgba(128,128,128,.06)";
973
1427
  var fg = inten > 0.5 ? "#fff" : "inherit";
974
- var tip = r3
1428
+ var tip = r3 && inR3
975
1429
  ? (function () {
976
1430
  var ds3 = daySplit(r3, regime, splitMap[key3]);
977
1431
  return dayLabel(key3) + t("\n调用 ") + r3.calls + t(" 次(高峰 ") + r3.peakCalls + t(" / 空闲 ") + r3.offPeakCalls + t(")\n输入·未命中 ") + fmtInt(r3.miss) + t(" · 缓存命中 ") + fmtInt(r3.hit) + t(" · 输出 ") + fmtInt(r3.out) + t(" · 推理 ") + fmtInt(r3.reason) + t("\n高峰消耗 ") + fmtMoney(ds3.peak) + t(" · 空闲消耗 ") + fmtMoney(ds3.off) + t(" · 合计 ") + fmtMoney(ds3.peak + ds3.off) + "(" + costHint(regime) + ")";
@@ -985,7 +1439,7 @@ var api = function (payload) {
985
1439
  style: Object.assign({ background: bg, color: fg }, st.calCell, isSel ? st.calCellOn : null)
986
1440
  },
987
1441
  el("div", null, String(d3)),
988
- r3 ? el("div", { style: st.calCellVal }, dimMode === "cost" ? fmtMoney(dayCost(r3)) : fmtInt(r3.calls)) : el("div", { style: st.calCellVal }, "—")
1442
+ r3 && inR3 ? el("div", { style: st.calCellVal }, dimMode === "cost" ? fmtMoney(dayCost(r3)) : fmtInt(r3.calls)) : el("div", { style: st.calCellVal }, "—")
989
1443
  ));
990
1444
  }
991
1445
 
@@ -1005,18 +1459,50 @@ var api = function (payload) {
1005
1459
  }
1006
1460
  }
1007
1461
 
1008
- // month daily stats (desc)
1462
+ // month daily stats (desc). 范围筛选激活时:跨月取范围内所有天数;否则取当前月。
1009
1463
  var monthRows = [];
1010
- for (var dd = 1; dd <= nDays; dd++) {
1011
- var kd = y + "-" + pad2(m + 1) + "-" + pad2(dd);
1012
- if (dayMap[kd]) monthRows.push(dayMap[kd]);
1464
+ if (rangeActive) {
1465
+ for (var dk in dayMap) {
1466
+ var dv = dayMap[dk];
1467
+ if (dv && inRangeKey(dv.day)) monthRows.push(dv);
1468
+ }
1469
+ } else {
1470
+ for (var dd = 1; dd <= nDays; dd++) {
1471
+ var kd = y + "-" + pad2(m + 1) + "-" + pad2(dd);
1472
+ if (dayMap[kd]) monthRows.push(dayMap[kd]);
1473
+ }
1013
1474
  }
1014
1475
  monthRows.sort(function (a, b) { return a.day < b.day ? 1 : a.day > b.day ? -1 : 0; });
1476
+ // 范围激活时:顶部卡片汇总也按范围内所有天重新计算(覆盖当前月汇总)
1477
+ if (rangeActive) {
1478
+ monthCalls = 0; monthMiss = 0; monthHit = 0; monthOut = 0; monthPeak = 0; monthOff = 0; monthPeakCalls = 0; monthOffCalls = 0;
1479
+ for (var dk2 = 0; dk2 < monthRows.length; dk2++) {
1480
+ var dv2 = monthRows[dk2];
1481
+ monthCalls += dv2.calls; monthMiss += dv2.miss; monthHit += dv2.hit; monthOut += dv2.out;
1482
+ monthPeakCalls += dv2.peakCalls || 0; monthOffCalls += dv2.offPeakCalls || 0;
1483
+ var dsx = daySplit(dv2, regime, splitMap[dv2.day]);
1484
+ monthPeak += dsx.peak; monthOff += dsx.off;
1485
+ }
1486
+ monthCost = monthPeak + monthOff;
1487
+ }
1015
1488
 
1016
1489
  function prevMonth() { setYm({ y: m === 0 ? y - 1 : y, m: m === 0 ? 11 : m - 1 }); setSelectedDay(null); }
1017
1490
  function nextMonth() { setYm({ y: m === 11 ? y + 1 : y, m: m === 11 ? 0 : m + 1 }); setSelectedDay(null); }
1018
1491
  function goToday() { setYm({ y: nowY, m: nowM }); setSelectedDay(bjKey(Date.now())); }
1019
1492
 
1493
+ function presetBtn(k, label) {
1494
+ return el("button", { style: preset === k ? st.segBtnOn : st.segBtn, onClick: function () { setPreset(k); if (k !== "custom") { setFromDate(""); setToDate(""); } } }, label);
1495
+ }
1496
+ function setCustomFrom(v) { setFromDate(v); setPreset("custom"); }
1497
+ function setCustomTo(v) { setToDate(v); setPreset("custom"); }
1498
+ function rangeHintText() {
1499
+ var from = fromDate, to = toDate;
1500
+ if (preset === "today") { from = nowKey; to = nowKey; }
1501
+ else if (preset === "7d") { from = bjKey(bjStartMs(nowKey) - 6 * 86400000); to = nowKey; }
1502
+ else if (preset === "30d") { from = bjKey(bjStartMs(nowKey) - 29 * 86400000); to = nowKey; }
1503
+ return (from ? dayLabel(from) : "…") + " – " + (to ? dayLabel(to) : "…");
1504
+ }
1505
+
1020
1506
  return el("div", null,
1021
1507
  el("div", { style: st.calBar },
1022
1508
  el("button", { style: st.btn, onClick: prevMonth }, t("‹ 上月")),
@@ -1028,6 +1514,16 @@ var api = function (payload) {
1028
1514
  el("button", { style: dimMode === "calls" ? st.segBtnOn : st.segBtn, onClick: function () { setDim("calls"); } }, t("按调用数"))
1029
1515
  )
1030
1516
  ),
1517
+ el("div", { style: st.calBar },
1518
+ el("div", { style: st.seg },
1519
+ presetBtn("today", t("今天")), presetBtn("7d", t("近7天")), presetBtn("30d", t("近30天")), presetBtn("all", t("全部"))
1520
+ ),
1521
+ el("span", { style: st.note }, t("自定义区间:")),
1522
+ el("input", { type: "date", style: st.dateInput, value: fromDate, onChange: function (e) { setCustomFrom(e.target.value); } }),
1523
+ el("span", { style: st.note }, t("至")),
1524
+ el("input", { type: "date", style: st.dateInput, value: toDate, onChange: function (e) { setCustomTo(e.target.value); } }),
1525
+ (fromDate || toDate || preset !== "all") ? el("button", { style: st.btn, onClick: function () { setFromDate(""); setToDate(""); setPreset("all"); } }, t("清除筛选")) : null
1526
+ ),
1031
1527
  el("div", { style: st.calGrid },
1032
1528
  weeks.map(function (w) { return el("div", { key: w, style: st.calWk }, w); }),
1033
1529
  cells
@@ -1039,17 +1535,19 @@ var api = function (payload) {
1039
1535
  el("span", null, t("· 悬停查看详情,点击某天查看当日调用"))
1040
1536
  ),
1041
1537
  el("div", { style: st.cards, marginTop: 12 },
1042
- el(Card, { label: t("本月调用"), value: fmtInt(monthCalls), hint: t("高峰 ") + fmtInt(monthPeakCalls) + t(" · 空闲 ") + fmtInt(monthOffCalls) + " · " + fmtMonthLabel(y, m + 1) }),
1538
+ el(Card, { label: rangeActive ? t("范围内调用") : t("本月调用"), value: fmtInt(monthCalls), hint: t("高峰 ") + fmtInt(monthPeakCalls) + t(" · 空闲 ") + fmtInt(monthOffCalls) + (rangeActive ? " · " + rangeHintText() : " · " + fmtMonthLabel(y, m + 1)) }),
1043
1539
  el(Card, { label: t("输入 · 未命中"), value: fmtInt(monthMiss), hint: "token" }),
1044
1540
  el(Card, { label: t("输入 · 缓存命中"), value: fmtInt(monthHit), hint: "token" }),
1045
1541
  el(Card, { label: t("输出"), value: fmtInt(monthOut), hint: "token" }),
1046
1542
  el(Card, { label: t("高峰消耗"), value: fmtMoney(monthPeak), hint: t("工作日高峰 9:00–12:00、14:00–18:00 · 周末全天空闲价") }),
1047
1543
  el(Card, { label: t("空闲消耗"), value: fmtMoney(monthOff), hint: t("其余空闲时段") }),
1048
- el(Card, { label: t("本月消耗"), value: fmtMoney(monthCost), hint: t("高峰 + 空闲 · ") + costHint(regime) })
1544
+ el(Card, { label: rangeActive ? t("范围内消耗") : t("本月消耗"), value: fmtMoney(monthCost), hint: t("高峰 + 空闲 · ") + costHint(regime) })
1049
1545
  ),
1050
- el("div", { style: st.sec }, dailyStatsTitle(y, m + 1)),
1546
+ el("div", { style: st.sec }, rangeActive
1547
+ ? (__LANG === "en" ? "Daily stats (" + rangeHintText() + ")" : t("每日统计(") + rangeHintText() + t(")"))
1548
+ : dailyStatsTitle(y, m + 1)),
1051
1549
  monthRows.length === 0
1052
- ? el("div", { style: st.empty }, t("本月暂无记录。"))
1550
+ ? el("div", { style: st.empty }, rangeActive ? t("该范围内暂无记录。") : t("本月暂无记录。"))
1053
1551
  : el("div", { style: st.scroll },
1054
1552
  el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
1055
1553
  el("thead", null, el("tr", null,
@@ -1123,6 +1621,8 @@ var api = function (payload) {
1123
1621
  var toDate = toState[0], setToDate = toState[1];
1124
1622
  var pageState = React.useState(1);
1125
1623
  var page = pageState[0], setPage = pageState[1];
1624
+ var queryState = React.useState("");
1625
+ var query = queryState[0], setQuery = queryState[1];
1126
1626
 
1127
1627
  var nowKey = bjKey(Date.now());
1128
1628
  var fromMs = null, toMs = null;
@@ -1134,11 +1634,16 @@ var api = function (payload) {
1134
1634
  if (toDate) toMs = bjStartMs(toDate) + 86400000 - 1;
1135
1635
  }
1136
1636
 
1637
+ var q = String(query || "").trim().toLowerCase();
1137
1638
  var filtered = [];
1138
1639
  for (var i = 0; i < records.length; i++) {
1139
1640
  var r = records[i];
1140
1641
  if (fromMs != null && r.time < fromMs) continue;
1141
1642
  if (toMs != null && r.time > toMs) continue;
1643
+ if (q && String(r.model || "").toLowerCase().indexOf(q) < 0
1644
+ && String(r.provider || "").toLowerCase().indexOf(q) < 0
1645
+ && String(r.sessionId || "").toLowerCase().indexOf(q) < 0
1646
+ && String(r.purpose || "").toLowerCase().indexOf(q) < 0) continue;
1142
1647
  filtered.push(r);
1143
1648
  }
1144
1649
  filtered.sort(function (a, b) { return b.time - a.time; }); // newest first
@@ -1186,12 +1691,16 @@ var api = function (payload) {
1186
1691
  el("input", { type: "date", style: st.dateInput, value: toDate, onChange: function (e) { setCustomTo(e.target.value); } }),
1187
1692
  (fromDate || toDate) ? el("button", { style: st.btn, onClick: function () { setFromDate(""); setToDate(""); setPreset("all"); setPage(1); } }, t("清除区间")) : null
1188
1693
  ),
1694
+ el("div", { style: st.calBar },
1695
+ el("input", { style: Object.assign({}, st.input, { minWidth: 220, flex: "1 1 220px" }), placeholder: t("按模型 / 服务商 / 会话搜索…"), value: query, onChange: function (e) { setQuery(e.target.value); setPage(1); } }),
1696
+ query ? el("button", { style: st.btn, onClick: function () { setQuery(""); setPage(1); } }, t("清除")) : null
1697
+ ),
1189
1698
  el("div", { style: st.note, marginTop: 8 },
1190
1699
  t("当前范围:共 ") + fmtInt(filtered.length) + t(" 条 · 输入·未命中 ") + fmtInt(sumMiss) + t(" · 缓存命中 ") + fmtInt(sumHit) + t(" · 缓存写入 ") + fmtInt(sumWrite) + t(" · 输出 ") + fmtInt(sumOut) +
1191
1700
  t(" · 高峰消耗 ") + fmtMoney(sums.peak) + t(" · 空闲消耗 ") + fmtMoney(sums.off) + t(" · 总消耗 ") + fmtMoney(sumCost) + "(" + costHint(regime) + ")"
1192
1701
  ),
1193
1702
  filtered.length === 0
1194
- ? el("div", { style: st.empty }, t("该时间范围内没有记录。"))
1703
+ ? el("div", { style: st.empty }, query || (fromDate || toDate || preset !== "all") ? t("没有符合条件的记录。") : t("该时间范围内没有记录。"))
1195
1704
  : el("div", null,
1196
1705
  pagerBar(),
1197
1706
  el("div", { style: st.scroll },
@@ -1289,7 +1798,36 @@ var api = function (payload) {
1289
1798
  );
1290
1799
  });
1291
1800
 
1801
+ var budget = props.budget || null;
1802
+ var budgetInputState = React.useState("");
1803
+ var budgetInput = budgetInputState[0], setBudgetInput = budgetInputState[1];
1804
+ var budgetMsgState = React.useState("");
1805
+ var budgetMsg = budgetMsgState[0], setBudgetMsg = budgetMsgState[1];
1806
+ function doSaveBudget() {
1807
+ var v = String(budgetInput || "").trim();
1808
+ if (!v) { setBudgetMsg(t("请输入预算金额")); return; }
1809
+ var n = Number(v);
1810
+ if (!Number.isFinite(n) || n < 0) { setBudgetMsg(t("预算金额必须是非负数字")); return; }
1811
+ setBudgetMsg(""); setBudgetInput("");
1812
+ if (props.onBudgetChange) props.onBudgetChange(n);
1813
+ }
1814
+ function doClearBudget() {
1815
+ setBudgetInput(""); setBudgetMsg("");
1816
+ if (props.onBudgetChange) props.onBudgetChange(null);
1817
+ }
1292
1818
  return el("div", null,
1819
+ el("div", { style: Object.assign({}, st.infobox, { marginTop: 0 }) },
1820
+ el("div", { style: st.infoboxTitle }, t("月度预算")),
1821
+ el("div", { style: st.note }, budget && budget.monthly > 0
1822
+ ? t("当前预算:") + fmtMoney(budget.monthly) + t(";概览页会按当月消耗显示进度与超支预警。")
1823
+ : t("设置每月消费上限,概览页会显示进度与超支预警。")),
1824
+ el("div", { style: st.actions, marginTop: 8 },
1825
+ el("input", { style: Object.assign({}, st.numInput, { width: 130, textAlign: "left" }), placeholder: t("预算金额(元)"), value: budgetInput, onChange: function (e) { setBudgetInput(e.target.value); } }),
1826
+ el("button", { style: st.btnPrimary, onClick: doSaveBudget }, t("保存预算")),
1827
+ budget && budget.monthly > 0 ? el("button", { style: st.btn, onClick: doClearBudget }, t("清除预算")) : null,
1828
+ budgetMsg ? el("span", { style: st.err }, budgetMsg) : null
1829
+ )
1830
+ ),
1293
1831
  el("div", { style: st.nowPeriod },
1294
1832
  el("span", { style: periodNow().peak ? st.badgePeak : st.badgeValley }, t("当前 · ") + t(periodNow().label)),
1295
1833
  el("span", { style: st.sub }, t(periodNow().desc))
@@ -1383,10 +1921,12 @@ var api = function (payload) {
1383
1921
 
1384
1922
  function UsagePanel(props) {
1385
1923
  var lang = useLang(); // 订阅语言变化:宿主切换语言时触发本面板重渲染(值本身仅作触发用)
1386
- var state = React.useState({ records: [], count: 0, dataPath: "", persistOk: false, persistError: "", pricing: null, days: [] });
1924
+ var state = React.useState({ records: [], count: 0, dataPath: "", persistOk: false, persistError: "", pricing: null, days: [], budget: null });
1387
1925
  var data = state[0], setData = state[1];
1388
1926
  var tabState = React.useState("overview");
1389
1927
  var tab = tabState[0], setTab = tabState[1];
1928
+ var infoState = React.useState(false);
1929
+ var infoOpen = infoState[0], setInfoOpen = infoState[1];
1390
1930
  var errState = React.useState("");
1391
1931
  var error = errState[0], setError = errState[1];
1392
1932
  var expState = React.useState("");
@@ -1403,12 +1943,18 @@ var api = function (payload) {
1403
1943
 
1404
1944
  function refresh() {
1405
1945
  api({ action: "list" }).then(function (res) {
1406
- setData(res || { records: [], count: 0, dataPath: "", persistOk: false, persistError: "", pricing: null, days: [] });
1946
+ setData(res || { records: [], count: 0, dataPath: "", persistOk: false, persistError: "", pricing: null, days: [], budget: null });
1407
1947
  setError("");
1408
1948
  setLoading(false);
1409
1949
  }).catch(function (e) { setError(String((e && e.message) || e)); setLoading(false); });
1410
1950
  }
1411
1951
 
1952
+ function saveBudget(monthly) {
1953
+ api({ action: "setBudget", monthly: monthly }).then(function (res) {
1954
+ if (res && res.ok) setData(function (prev) { return Object.assign({}, prev, { budget: res.budget }); });
1955
+ }).catch(function () {});
1956
+ }
1957
+
1412
1958
  React.useEffect(function () {
1413
1959
  refresh();
1414
1960
  // 每 10 秒自动刷新(列表已分页渲染,避免 3 秒全量重取 + 全量重渲染导致的卡顿)
@@ -1515,45 +2061,46 @@ var api = function (payload) {
1515
2061
  }, t(sub.z));
1516
2062
  })
1517
2063
  ),
1518
- el("div", { style: st.diffbox },
1519
- el("div", { style: st.diffboxTitle }, t("本地统计与官方后台的差异")),
1520
- el("div", { style: st.diffboxText }, t("本面板统计的是插件在本地捕获的模型调用(按官方价格与峰谷时段计费)。与官方后台(platform.deepseek.com 用量页)相比,金额可能更低,常见原因:")),
1521
- el("div", { style: st.diffboxText }, t("① 中断/出错/超时的调用:官方仍按实际 token 计费,插件无法获取其用量,按 0 记录(明细中标「中断」);")),
1522
- el("div", { style: st.diffboxText }, t("② 账号下其他 API Key(如其它应用/脚本)的调用不经过 DeepSeek Harness,官方统计包含它们,本插件不包含;")),
1523
- el("div", { style: st.diffboxText }, t("③ 如需精确对账,可在官方用量页导出月度账单 CSV 与本插件对比。")),
1524
- interruptedTotal > 0
1525
- ? el("div", { style: st.diffboxWarn }, t("当前记录中有 ") + fmtInt(interruptedTotal) + t(" 次中断调用(未计费)——对应官方后台已计费的部分。"))
1526
- : null
1527
- ),
1528
- el(BillingHint, null),
1529
- el("div", { style: st.note, marginTop: 6 },
1530
- t("计价说明:DeepSeek 官方与 SiliconFlow 按人民币价格;DigitalOcean 按美元官方价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。")
2064
+ el("div", { style: st.calBar, marginTop: 8 },
2065
+ el("button", { style: infoOpen ? st.btnPrimary : st.btn, onClick: function () { setInfoOpen(!infoOpen); } }, (infoOpen ? t("收起帮助") : t("帮助与说明")) + (interruptedTotal > 0 ? " · " + fmtInt(interruptedTotal) : ""))
1531
2066
  ),
2067
+ infoOpen
2068
+ ? el("div", { style: st.diffbox },
2069
+ el("div", { style: st.diffboxTitle }, t("本地统计与官方后台的差异")),
2070
+ el("div", { style: st.diffboxText }, t("本面板统计的是插件在本地捕获的模型调用(按官方价格与峰谷时段计费)。与官方后台(platform.deepseek.com 用量页)相比,金额可能更低,常见原因:")),
2071
+ el("div", { style: st.diffboxText }, t("① 中断/出错/超时的调用:官方仍按实际 token 计费,插件无法获取其用量,按 0 记录(明细中标「中断」);")),
2072
+ el("div", { style: st.diffboxText }, t("② 账号下其他 API Key(如其它应用/脚本)的调用不经过 DeepSeek Harness,官方统计包含它们,本插件不包含;")),
2073
+ el("div", { style: st.diffboxText }, t("③ 如需精确对账,可在官方用量页导出月度账单 CSV 与本插件对比。")),
2074
+ interruptedTotal > 0
2075
+ ? el("div", { style: st.diffboxWarn }, t("当前记录中有 ") + fmtInt(interruptedTotal) + t(" 次中断调用(未计费)——对应官方后台已计费的部分。"))
2076
+ : null,
2077
+ el(BillingHint, null),
2078
+ el("div", { style: st.diffboxText, marginTop: 6 },
2079
+ t("计价说明:DeepSeek 官方与 SiliconFlow 按人民币价格;DigitalOcean 按美元官方价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。"))
2080
+ )
2081
+ : null,
1532
2082
  loading
1533
2083
  ? el(LoadingView, null)
1534
2084
  : el("div", null,
1535
- tab === "overview" ? el(OverviewView, { records: records, regime: "auto", toolCalls: data.toolCalls || [] }) : null,
2085
+ tab === "overview" ? el(OverviewView, { records: records, regime: "auto", toolCalls: data.toolCalls || [], days: data.days || [], budget: data.budget }) : null,
1536
2086
  tab === "calendar" ? el(CalendarView, { records: records, regime: "auto", days: data.days || [] }) : null,
1537
2087
  tab === "cache" ? el(CacheListView, { records: records, regime: "auto" }) : null,
1538
- 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
2088
+ tab === "prices" ? el(PriceView, { pricing: data.pricing, effectiveAt: data.effectiveAt, fx: data.fx || {}, budget: data.budget, onBudgetChange: saveBudget, onFxRefresh: function () { api({ action: "fxRefresh" }).then(refresh).catch(function () {}); }, onChanged: refresh }) : null
1539
2089
  ),
1540
- el("div", { style: st.actions, marginTop: 4 },
2090
+ el("div", { style: st.actions, marginTop: 8 },
1541
2091
  el("button", { style: st.btn, onClick: function () { doExport("csv"); } }, t("导出 CSV")),
1542
2092
  el("button", { style: st.btn, onClick: function () { doExport("json"); } }, t("导出 JSON")),
1543
2093
  el("button", { style: st.btn, onClick: doExportImage }, t("导出图片 (PNG)")),
1544
2094
  el("button", { style: st.btn, onClick: doReveal }, t("打开目录")),
1545
- exportMsg ? el("span", { style: st.sub }, __msgText(exportMsg)) : null
2095
+ el("button", { style: st.btn, onClick: pickFile }, t("选择文件导入")),
2096
+ exportMsg ? el("span", { style: st.sub }, __msgText(exportMsg)) : null,
2097
+ importMsg ? el("span", { style: st.sub }, __msgText(importMsg)) : null
1546
2098
  ),
1547
2099
  el("div", { style: st.actions },
1548
2100
  el("input", { style: st.input, placeholder: t("导出目标目录(留空 = 默认数据目录)"), value: destDir, onChange: function (e) { setDestDir(e.target.value); } }),
1549
2101
  el("button", { style: st.btn, onClick: pickDestDir }, t("选择目录…")),
1550
2102
  destDir ? el("button", { style: st.btn, onClick: function () { setDestDir(""); setExportMsg({ k: "已恢复默认数据目录" }); } }, t("重置")) : null
1551
2103
  ),
1552
- el("div", { style: st.actions },
1553
- el("input", { type: "file", accept: ".json,.csv", style: { display: "none" }, ref: function (n) { fileInputNode = n; }, onChange: onFileChange }),
1554
- el("button", { style: st.btn, onClick: pickFile }, t("选择文件导入")),
1555
- importMsg ? el("span", { style: st.sub }, __msgText(importMsg)) : null
1556
- ),
1557
2104
  error ? el("div", { style: st.err }, error) : null,
1558
2105
  dataPath
1559
2106
  ? el("div", { style: st.note }, t("数据持久化:") + dataPath + t("(每次调用实时落盘,插件重启后自动恢复,最多保留 100000 条)"))
package/lib/index.js CHANGED
@@ -303,6 +303,8 @@ export default {
303
303
  let root = ''
304
304
  let dataPath = ''
305
305
  let pricingPath = ''
306
+ let budgetPath = ''
307
+ let budget = null // { monthly: number|null } — 月度预算(元),null 表示未设置
306
308
  let persistOk = false
307
309
  let persistError = ''
308
310
  let initPromise = null
@@ -363,6 +365,21 @@ export default {
363
365
  return nfsWriteText(pricingPath, JSON.stringify(PRICING)).catch(() => {})
364
366
  }
365
367
 
368
+ function persistBudget() {
369
+ if (!budgetPath || !persistOk) return Promise.resolve()
370
+ return nfsWriteText(budgetPath, JSON.stringify(budget)).catch(() => {})
371
+ }
372
+
373
+ async function loadBudget() {
374
+ if (!budgetPath) return
375
+ try {
376
+ const data = JSON.parse(await nfsReadText(budgetPath))
377
+ if (!data || typeof data !== 'object') return
378
+ const m = Number(data.monthly)
379
+ budget = { monthly: Number.isFinite(m) && m >= 0 ? m : null }
380
+ } catch (e) {}
381
+ }
382
+
366
383
  async function loadPricing() {
367
384
  if (!pricingPath) return
368
385
  try {
@@ -433,7 +450,9 @@ export default {
433
450
  root = normPath(candidate)
434
451
  dataPath = tryPath
435
452
  pricingPath = joinPath(path.dirname(dataPath), 'pricing.json')
453
+ budgetPath = joinPath(path.dirname(dataPath), 'budget.json')
436
454
  await loadPricing()
455
+ await loadBudget()
437
456
  persistOk = true
438
457
  persistError = ''
439
458
  return { ok: true }
@@ -1214,7 +1233,7 @@ export default {
1214
1233
  try { await refreshFxRate(false) } catch (e) {}
1215
1234
  const items = records.filter(isRealCall).map(projectRecord)
1216
1235
  const toolItems = records.filter((r) => !isRealCall(r)).map(projectRecord)
1217
- return { ok: true, records: items, count: items.length, toolCalls: toolItems, dataPath, persistOk, persistError, pricing: PRICING, effectiveAt: EFFECTIVE_AT, days: buildDays(), fx: FX }
1236
+ return { ok: true, records: items, count: items.length, toolCalls: toolItems, dataPath, persistOk, persistError, pricing: PRICING, effectiveAt: EFFECTIVE_AT, days: buildDays(), fx: FX, budget }
1218
1237
  }
1219
1238
  case 'tokenForMessage': {
1220
1239
  // 消息底部弹窗:分两层统计——整场对话累计(conversation)+ 本轮明细(aggregate/records)。
@@ -1272,6 +1291,14 @@ export default {
1272
1291
  persistPricing()
1273
1292
  return { ok: true }
1274
1293
  }
1294
+ case 'setBudget': {
1295
+ // 设置月度预算(元);传入非负数字生效,null/NaN/负数视为清除预算。
1296
+ const raw = Number(body && body.monthly)
1297
+ if (Number.isFinite(raw) && raw >= 0) budget = { monthly: raw }
1298
+ else budget = null
1299
+ persistBudget()
1300
+ return { ok: true, budget }
1301
+ }
1275
1302
  case 'fxRefresh': {
1276
1303
  const fx = await refreshFxRate(true)
1277
1304
  return { ok: fx.rate > 0, fx, error: fx.rate > 0 ? '' : (fx.error || '无法获取汇率') }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feiyang666/dsh-usage-plugin",
3
- "version": "1.14.1",
3
+ "version": "1.16.5",
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",