dsh-token-plugin 0.0.0-stage → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,2485 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-token-plugin",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ var React = require("react");
8
+ var el = React.createElement;
9
+ // ── i18n: zh keys → en dictionary (community addition) ──
10
+ 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","V4.1 调价":"V4.1 price change"," 2026-09-10 12:00(北京时间)起 deepseek-flash 按新版峰谷价计费(deepseek-v4-pro 不变)":" from 2026-09-10 12:00 Beijing time deepseek-flash uses the new peak/off-peak prices (deepseek-v4-pro unchanged)","官方来源与价格依据":"Official sources & price references","价格由 DeepSeek 官方公布,本插件只做本地计算、不修改也不代理官方价格;点下面的链接可直接核对官方原文。":"Prices are published by DeepSeek; this plugin only computes locally and neither changes nor proxies official pricing. Use the links below to verify against the official source.","DeepSeek 官方价格页(模型与价格、峰谷时段定义)":"DeepSeek official pricing page (models, prices, peak/off-peak hours)","DeepSeek V4.1 Flash 发布公告":"DeepSeek V4.1 Flash announcement","DeepSeek 开放平台用量页(官方账单,用于对账)":"DeepSeek platform usage page (official billing, for reconciliation)","DeepSeek 官方 API 文档":"DeepSeek official API documentation","DeepSeek Harness 官方仓库":"DeepSeek Harness official repository","为什么显示这个价格?":"Why is this the price shown?","① 官方价格表按生效时间分段:2026-08-17 00:00 起为第一版峰谷价;2026-09-10 12:00(北京时间)起为 DeepSeek V4.1 Flash 调价后的新版(deepseek-flash 单价下调,deepseek-v4-pro 不变)。每次调用按其发生时刻对应的版本计费,历史金额不会因后续调价而改变。":"① The official price table is split by effective date: the first peak/off-peak version applies from 2026-08-17 00:00, and the post-V4.1-Flash version from 2026-09-10 12:00 Beijing time (deepseek-flash prices are lowered; deepseek-v4-pro is unchanged). Every call is billed with the version in force at its own time, so past amounts never change when prices are adjusted later.","② 模型档位按官方新命名归并:旧名 deepseek-v4-flash、deepseek-v4-flash-vision-exp 仍可调用(官方已路由到 DeepSeek-V4.1-Flash 并按 Flash 单价计费),计费时与 deepseek-flash 合并为同一档位。":"② Model tiers follow the new official naming: the legacy ids deepseek-v4-flash and deepseek-v4-flash-vision-exp stay callable (DeepSeek routes them to DeepSeek-V4.1-Flash and bills them at the Flash price) and are merged into the same deepseek-flash tier for billing.","③ 峰谷规则:工作日 9:00–12:00、14:00–18:00(北京时间)为高峰,其余时间(含周末全天)为空闲;空闲价 = 高峰价的一半。":"③ Peak/off-peak rule: weekdays 9:00–12:00 and 14:00–18:00 Beijing time are peak; all other times (including the whole weekend) are off-peak; the off-peak price is half the peak price.","④ 只有能与记录中 provider / model 可靠匹配的价格才参与计费,其余按 ¥0 统计(避免用相近模型的价格误算)。":"④ Only prices that reliably match the recorded provider / model are billed; everything else counts as ¥0, so a similar model name never causes a wrong estimate.","⑤ 本插件按官方公布价格在本地计算,结果仅供参考,最终以 DeepSeek 官方账单为准;产品价格可能变动,请以官方价格页为准。":"⑤ This plugin computes locally from published prices; the result is for reference only and the official DeepSeek bill prevails. Prices may change, so always check the official pricing page.","建议:若新模型(如 deepseek-flash)的消耗显示为 ¥0,通常是本插件版本过旧——请升级到 1.17.0 或更高版本。":"Tip: if a new model (such as deepseek-flash) shows ¥0 cost, this plugin is usually out of date — please upgrade to 1.17.0 or later.","DeepSeek V4.1 Flash 调价将于 ":"The DeepSeek V4.1 Flash price change takes effect on ","(北京时间)生效:deepseek-flash 改用新版峰谷价(空闲 缓存命中 0.02 / 输入 1 / 输出 4 元,高峰为其两倍),deepseek-v4-pro 价格不变。":" (Beijing time): deepseek-flash switches to the new peak/off-peak prices (off-peak cache hit 0.02 / input 1 / output 4 CNY; peak is twice that), while deepseek-v4-pro stays unchanged.","DeepSeek V4.1 Flash 调价已生效(自 ":"The DeepSeek V4.1 Flash price change is in effect (since "," 起):deepseek-flash 按新版峰谷价计费,deepseek-v4-pro 价格不变。":"): deepseek-flash is billed at the new peak/off-peak prices; deepseek-v4-pro is unchanged.","峰谷价表按生效时间分段:2026-08-17 00:00 起为第一版峰谷价;2026-09-10 12:00 起为 DeepSeek V4.1 Flash 调价后的新版(deepseek-flash 空闲价下调为 缓存命中 0.02 / 输入 1 / 输出 4 元,高峰为其两倍;deepseek-v4-pro 价格不变)。上方「峰谷价」展示的是当前生效的一版,历史调用始终按其发生时刻对应的版本计费。":"The peak/off-peak tables are split by effective date: the first version applies from 2026-08-17 00:00, and the post-V4.1-Flash version applies from 2026-09-10 12:00 (deepseek-flash off-peak prices are lowered to cache hit 0.02 / input 1 / output 4 CNY, with peak at twice that; deepseek-v4-pro is unchanged). The Peak/Off-peak tab shows the currently effective version; past calls are always billed with the version in force at their own time.","旧模型名 deepseek-v4-flash 与 deepseek-v4-flash-vision-exp 仍可调用(官方已将其路由到 DeepSeek-V4.1-Flash、按 Flash 单价计费),在本价格表中与 deepseek-flash 合并为同一档位。":"The legacy model ids deepseek-v4-flash and deepseek-v4-flash-vision-exp remain callable (they are routed to DeepSeek-V4.1-Flash and billed at the Flash price) and are merged into the deepseek-flash row in this table.","无 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.","统计口径:web_search 搜索后端未纳入精确统计":"Scope: web_search backend calls are not precisely metered","DeepSeek 搜索后端调用由 Harness 用原生请求直连官方接口,响应用量未持久化到会话日志,插件无法取到实际 token。此处只记录次数,并按请求体给出输入侧下限估算:共 ":"DeepSeek search-backend calls go straight to the official endpoint via a native request inside the Harness, and the response usage is never persisted to the session log, so the plugin cannot obtain the real token counts. Only the call count is recorded here, together with a lower-bound estimate of the input side derived from the request body: "," 次,估算消耗 ≥ ":" calls, estimated cost ≥ ","。搜索在服务端注入检索结果并可能多轮生成,输入与输出都会高于此值,官方账单必然更高。":". Search results are injected server-side and generation may run multiple rounds, so both input and output exceed this bound — the official bill will always be higher.","该部分为估算下限,请以 DeepSeek 官方后台账单为准。":"This part is a lower-bound estimate; always refer to your DeepSeek console bill.","历史价格提示":"Historical pricing notice","有 ":"There are "," 条记录没有冻结单价(旧版本写入或手工导入),它们的费用按当前价格表计算,改动价格表后会跟着变化;新记录已冻结写入时生效的单价,不受后续改价影响。":" record(s) have no frozen unit price (written by an older version or imported by hand); their cost is computed with the current price table and will change when you edit prices. New records freeze the prices in effect at write time and are unaffected by later edits.","历史价格与重算":"Historical pricing & repricing"," 条记录没有冻结单价(旧版本写入或手工导入),其费用按当前价格表计算,改动价格后会跟着变化。":" record(s) have no frozen unit price (written by an older version or imported by hand); their cost uses the current price table and follows later price edits.","所有记录都已冻结写入时生效的单价:改动价格表只影响之后产生的记录,历史费用不会被改写。":"Every record has frozen the prices in effect when it was written: editing the price table only affects future records, past costs are never rewritten.","用当前价格表重算全部历史":"Reprice all history with the current table","已提交,正在用当前价格表重算全部历史…":"Submitted — repricing all history with the current table…","当前宿主不支持重算":"This host does not support repricing"};
11
+
12
+ function __detectLang() {
13
+ // 语言完全跟随宿主设置(通用设置 → 语言):不再读 localStorage,避免旧选择覆盖系统语言
14
+ try { var nav = (typeof navigator !== "undefined" && navigator.language) || "en"; return nav.toLowerCase().indexOf("zh") === 0 ? "zh" : "en"; } catch (e2) { return "en"; }
15
+ }
16
+ var __LANG = __detectLang();
17
+ var t = function (s) { if (__LANG !== "en") return s; return __T_EN[s] || s; };
18
+ // 1.19.0 新增文案(DeepSeek 多币种余额、百炼按量付费)。合并进主表而不是去改
19
+ // 上面那一整行巨型 JSON,改动即可在 diff 里逐条看到。
20
+ Object.assign(__T_EN, {
21
+ " · 总计": " · total",
22
+ "充值 ": "Top-up ",
23
+ " · 赠送 ": " · granted ",
24
+ "查询 DeepSeek、SiliconFlow、DigitalOcean、百炼 Token Plan 与百炼按量付费": "Query DeepSeek, SiliconFlow, DigitalOcean, Bailian Token Plan and Bailian pay-as-you-go",
25
+ "百炼按量付费余额需在阿里云费用中心查看;百炼模型 API Key 无法查询按量付费余额": "Pay-as-you-go balance lives in the Alibaba Cloud billing console; Bailian model API keys cannot query it",
26
+ "插件不会尝试未经公开文档确认的端点,也不会把推理 Key 当作账单凭据。": "The plugin does not probe endpoints that are not publicly documented, and never treats an inference key as a billing credential.",
27
+ "价格来源与快照": "Price source & snapshots",
28
+ "官方价目页核对日期:": "Official pricing page checked on: ",
29
+ "新增 Search / Fetch 按次计费 ¥5 / 1000 次成功请求:本插件拿不到该调用次数,未计入费用。": "New per-request billing for Search / Fetch (¥5 per 1,000 successful requests): the plugin cannot see that call count, so it is not included in the cost.",
30
+ "法定节假日已纳入峰谷判定(自 ": "Legal holidays are now part of the peak/off-peak rule (effective from ",
31
+ " 起生效)。": ").",
32
+ "历史价格记录(": "Price snapshots (",
33
+ " 条)": ")",
34
+ "核对官方价格页": "Verify on the official pricing page",
35
+ "暂无历史快照;首次改价后会自动记录。": "No snapshots yet; one is recorded automatically the first time prices change.",
36
+ "当前运行的插件版本": "Currently running plugin version",
37
+ "插件版本 ": "Plugin version ",
38
+ " · 数据目录 ": " · Data directory ",
39
+ // 1.21.0 UI 瘦身新增的文案
40
+ "口径与版本": "Scope & version",
41
+ "收起说明": "Hide details",
42
+ "导出与数据": "Export & data",
43
+ "收起数据": "Hide data",
44
+ " 次中断": " interrupted",
45
+ "统计口径提示": "Metering notice",
46
+ " 次估算": " estimated",
47
+ " 条无冻结单价": " without frozen unit price",
48
+ "金额以 DeepSeek 官方后台账单为准": "Amounts follow the official DeepSeek billing console",
49
+ "未命中 ": "Miss ",
50
+ " 个服务商": " providers",
51
+ "消耗明细": "Cost breakdown",
52
+ "按服务商": "By provider",
53
+ "用量": "Usage",
54
+ "日历": "Calendar",
55
+ "明细": "Details",
56
+ "设置": "Settings",
57
+ "缓存命中明细": "Cache-hit details",
58
+ "价格口径与第三方价格": "Pricing rules & third-party prices",
59
+ "计价规则与官方价格依据见「价格表」页;费用以 DeepSeek 官方后台账单为准。": "Pricing rules and official references live on the Price table tab; amounts follow the official DeepSeek billing console.",
60
+ " · flash 空闲价 ": " · flash off-peak "
61
+ });
62
+ var __msgText = function (m) {
63
+ if (!m) return "";
64
+ if (typeof m === "string") return m;
65
+ if (m.seq) {
66
+ var acc = "";
67
+ for (var si = 0; si < m.seq.length; si++) {
68
+ var part = m.seq[si];
69
+ acc += (part && typeof part === "object" && part.k !== undefined) ? t(part.k) : String(part);
70
+ }
71
+ return acc;
72
+ }
73
+ var base = m.k ? t(m.k) : (m.raw || "");
74
+ if (m.tail !== undefined && m.tail !== null) base += String(m.tail);
75
+ return base;
76
+ };
77
+ var __msgText = function (m) {
78
+ if (!m) return "";
79
+ if (typeof m === "string") return m;
80
+ if (m.seq) {
81
+ var acc = "";
82
+ for (var si = 0; si < m.seq.length; si++) {
83
+ var part = m.seq[si];
84
+ acc += (part && typeof part === "object" && part.k !== undefined) ? t(part.k) : String(part);
85
+ }
86
+ return acc;
87
+ }
88
+ var base = m.k ? t(m.k) : (m.raw || "");
89
+ if (m.tail !== undefined && m.tail !== null) base += String(m.tail);
90
+ return base;
91
+ };
92
+ var __langListeners = [];
93
+ var setLang = function (lang) {
94
+ var next = lang === "zh" ? "zh" : "en";
95
+ if (next === __LANG) return __LANG;
96
+ __LANG = next;
97
+ // 仅内存生效:不再写 localStorage,语言始终以宿主设置为准
98
+ for (var i = 0; i < __langListeners.length; i++) { try { __langListeners[i](__LANG); } catch (e2) {} }
99
+ return __LANG;
100
+ };
101
+ // 跟随宿主语言服务(通用设置 → 语言):读取当前语言并订阅变化,切换后即时生效
102
+ var syncWithSystemLocale = function (locale) {
103
+ if (!locale || typeof locale.getLocale !== "function") return;
104
+ var sync = function () {
105
+ var snap = locale.getLocale();
106
+ setLang(snap && snap.active === "zh" ? "zh" : "en");
107
+ };
108
+ sync();
109
+ if (typeof locale.subscribe === "function") locale.subscribe(sync);
110
+ };
111
+ function useLang() {
112
+ var st = React.useState(__LANG);
113
+ React.useEffect(function () {
114
+ var fn = function () { st[1](__LANG); };
115
+ __langListeners.push(fn);
116
+ return function () { var idx = __langListeners.indexOf(fn); if (idx >= 0) __langListeners.splice(idx, 1); };
117
+ }, []);
118
+ return st[0];
119
+ }
120
+
121
+ // ── inline styles (replaces a stylesheet; portable across client plugins) ──
122
+ // 字号用 em 表示,随宿主“显示大小”设置的字体基准自动缩放(继承应用字号);
123
+ // 间距保持 px,仅微调以适配更大的字号。标注的 px 是默认 13px 基准下的设计值。
124
+ var BASE_FS = 13;
125
+ var fs = function (n) { return (Math.round(n / BASE_FS * 100) / 100).toFixed(2) + "em"; };
126
+
127
+ // ── 设计令牌 ────────────────────────────────────────────────────────────
128
+ // 组件层只允许引用这里的 token,不再散落 #3a7bd5 / rgba(90,140,255,.18) 这类裸值:
129
+ // 同一个"蓝"此前有 4 个色号、同一个"红"有 3 个,深浅色主题下对比度也不一致。
130
+ // 语义收敛为四类:ok(成功 / 缓存命中)、warn(需要注意 / 高峰)、
131
+ // danger(错误 / 超支)、accent(主操作 / 链接 / 中性强调)。
132
+ var C = {
133
+ ok: "#22a45d",
134
+ okBg: "rgba(34,164,93,.16)",
135
+ warn: "#c2760b",
136
+ warnBg: "rgba(194,118,11,.10)",
137
+ warnLine: "rgba(194,118,11,.38)",
138
+ danger: "#e5484d",
139
+ dangerBg: "rgba(229,72,77,.09)",
140
+ dangerLine: "rgba(229,72,77,.40)",
141
+ accent: "#4b7bec",
142
+ accentText: "#3d6bd6",
143
+ accentBg: "rgba(75,123,236,.16)",
144
+ accentSoft: "rgba(75,123,236,.06)",
145
+ accentLine: "rgba(75,123,236,.40)",
146
+ line: "rgba(128,128,128,.22)",
147
+ lineSoft: "rgba(128,128,128,.15)",
148
+ lineStrong: "rgba(128,128,128,.35)",
149
+ fill: "rgba(128,128,128,.06)",
150
+ fillStrong: "rgba(128,128,128,.09)"
151
+ };
152
+ // 字号阶梯:只保留 6 档(10 / 11 / 12 / 13 / 15 / 20 + 32 展示值),
153
+ // 不再出现 10.5、14、18、22、26 这类随手写的中间值。
154
+ var FS = { xs: 10, sm: 11, md: 12, lg: 13, xl: 15, xxl: 20, display: 32 };
155
+ // 渐变与"叠加在品牌色上的白色元素":这些不是语义色,但同样不该散落成裸字符串。
156
+ var G = {
157
+ brand: "linear-gradient(135deg," + C.accentText + ",#00d2ff)",
158
+ onAccent: "linear-gradient(90deg,#ffffff,#d7ecff)",
159
+ budgetWarn: "linear-gradient(90deg,#ffe08a,#ffb347)",
160
+ budgetDanger: "linear-gradient(90deg,#ff9a8b," + C.danger + ")",
161
+ inkOnAccent: "#ffffff",
162
+ chipDark: "#15171c"
163
+ };
164
+
165
+ var st = {
166
+ root: { display: "flex", flexDirection: "column", gap: 14, padding: "4px 0", width: "100%", maxWidth: "100%", minWidth: 0, boxSizing: "border-box" },
167
+ tab: { padding: "16px 20px", maxWidth: "min(1200px, calc(100vw - 24px))", margin: "0 auto", width: "100%", minWidth: 0, boxSizing: "border-box" },
168
+ head: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap" },
169
+ headleft: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" },
170
+ title: { fontSize: fs(FS.xl), fontWeight: 600, display: "flex", alignItems: "center", gap: 8 },
171
+ titleVer: { fontSize: fs(FS.xs), fontWeight: 500, opacity: 0.5, letterSpacing: 0, padding: "1px 6px", border: "1px solid rgba(128,128,128,.35)", borderRadius: 999, whiteSpace: "nowrap" },
172
+ sub: { fontSize: fs(FS.sm), opacity: 0.55 },
173
+ actions: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" },
174
+ btn: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 6, padding: "4px 10px", fontSize: fs(FS.md), cursor: "pointer", color: "inherit" },
175
+ btnPrimary: { border: "1px solid rgba(90,140,255,.6)", background: "rgba(90,140,255,.22)", borderRadius: 6, padding: "4px 10px", fontSize: fs(FS.md), cursor: "pointer", color: "inherit" },
176
+ btnDisabled: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 6, padding: "4px 10px", fontSize: fs(FS.md), cursor: "default", color: "inherit", opacity: 0.5 },
177
+ input: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 6, padding: "4px 10px", fontSize: fs(FS.md), color: "inherit", minWidth: 280 },
178
+ dateInput: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 6, padding: "3px 8px", fontSize: fs(FS.md), color: "inherit" },
179
+ seg: { display: "inline-flex", border: "1px solid rgba(128,128,128,.35)", borderRadius: 6, overflow: "hidden", flexWrap: "wrap" },
180
+ segBtn: { border: 0, background: "transparent", padding: "4px 10px", fontSize: fs(FS.md), cursor: "pointer", color: "inherit" },
181
+ segBtnOn: { border: 0, background: "rgba(90,140,255,.22)", padding: "4px 10px", fontSize: fs(FS.md), cursor: "pointer", color: "inherit", fontWeight: 600 },
182
+ cards: { display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(170px,1fr))", gap: 10 },
183
+ card: { border: "1px solid rgba(128,128,128,.35)", borderRadius: 8, padding: "10px 12px", minWidth: 0 },
184
+ cardL: { fontSize: fs(FS.sm), opacity: 0.6, marginBottom: 4 },
185
+ cardV: { fontSize: fs(FS.xxl), fontWeight: 600, overflowWrap: "anywhere" },
186
+ cardH: { fontSize: fs(FS.xs), opacity: 0.5, marginTop: 2, overflowWrap: "anywhere" },
187
+ sec: { fontSize: fs(FS.lg), fontWeight: 600, marginTop: 6 },
188
+ subtabBar: { display: "flex", gap: 6, borderBottom: "1px solid rgba(128,128,128,.2)", paddingBottom: 8, flexWrap: "wrap" },
189
+ subtab: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 6, padding: "5px 14px", fontSize: fs(FS.md), cursor: "pointer", color: "inherit" },
190
+ subtabOn: { border: "1px solid rgba(90,140,255,.5)", background: "rgba(90,140,255,.22)", borderRadius: 6, padding: "5px 14px", fontSize: fs(FS.md), cursor: "pointer", color: "inherit", fontWeight: 600 },
191
+ scroll: { overflowX: "auto", overflowY: "hidden", width: "100%", maxWidth: "100%", minWidth: 0 },
192
+ // 表格按内容撑开(width:max-content),列不被压缩,数值不会换行/重叠;
193
+ // 外层 st.scroll(overflow-x:auto)在窗口不够宽时横向滚动。minWidth:720 兜底极窄屏。
194
+ tbl: { width: "max-content", minWidth: 720, borderCollapse: "collapse", fontSize: fs(FS.md) },
195
+ th: { textAlign: "right", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word", opacity: 0.55, fontWeight: 500, lineHeight: 1.5 },
196
+ thFirst: { textAlign: "left", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word", opacity: 0.55, fontWeight: 500, lineHeight: 1.5 },
197
+ td: { textAlign: "right", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word" },
198
+ tdFirst: { textAlign: "left", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word" },
199
+ tdWrap: { textAlign: "left", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word" },
200
+ // tdTotal / tdTotalFirst 在下方统一重新定义(合计行用更醒目的强调色),
201
+ // 这里不再重复声明——同名键重复会让"改了却不生效"变成常见坑。
202
+ tdClick: { textAlign: "left", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word", cursor: "pointer" },
203
+ tdClickSel: { textAlign: "left", padding: "7px 8px", borderBottom: "1px solid " + C.line, whiteSpace: "normal", wordBreak: "break-word", cursor: "pointer", color: C.accent, fontWeight: 600 },
204
+ tdGroup: { textAlign: "left", padding: "7px 8px", fontWeight: 600, background: C.fillStrong, borderBottom: "1px solid " + C.line, whiteSpace: "normal", wordBreak: "break-word" },
205
+ tdGroupR: { textAlign: "right", padding: "7px 8px", fontWeight: 600, background: C.fillStrong, borderBottom: "1px solid " + C.line, whiteSpace: "normal", wordBreak: "break-word" },
206
+ badgeHit: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(FS.sm), fontWeight: 500, background: C.okBg, color: C.ok, whiteSpace: "nowrap" },
207
+ badgePeak: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(FS.sm), fontWeight: 500, background: C.warnBg, color: C.warn, whiteSpace: "nowrap" },
208
+ badgeValley: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(FS.sm), fontWeight: 500, background: C.accentBg, color: C.accent, whiteSpace: "nowrap" },
209
+ // 概览顶部「一行摘要」:替代原先占半屏的蓝色 hero 卡
210
+ summaryStrip: { display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap", marginTop: 10, padding: "9px 12px", borderRadius: 10, border: "1px solid rgba(128,128,128,.18)", background: "rgba(128,128,128,.05)" },
211
+ summaryMain: { display: "flex", alignItems: "baseline", gap: 8, flexWrap: "wrap" },
212
+ summaryLabel: { fontSize: fs(FS.sm), opacity: 0.6 },
213
+ summaryValue: { fontSize: fs(FS.xxl), fontWeight: 700, letterSpacing: ".2px" },
214
+ summarySub: { fontSize: fs(FS.sm), opacity: 0.6 },
215
+ summaryBudget: { display: "flex", alignItems: "center", gap: 8, minWidth: 220 },
216
+ scopeBar: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", marginTop: 10 },
217
+ badgeWarn: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(FS.sm), fontWeight: 500, background: C.dangerBg, color: C.danger, whiteSpace: "nowrap" },
218
+ costPeak: { color: C.warn, fontWeight: 600, whiteSpace: "nowrap" },
219
+ costOff: { color: C.accentText, fontWeight: 600, whiteSpace: "nowrap" },
220
+ err: { color: C.danger, fontSize: fs(FS.md) },
221
+ empty: { fontSize: fs(FS.md), opacity: 0.6, padding: "16px 0" },
222
+ note: { fontSize: fs(FS.sm), opacity: 0.55 },
223
+ errbox: { border: "1px solid " + C.dangerLine, background: C.dangerBg, borderRadius: 8, padding: 12, fontSize: fs(FS.md) },
224
+ errboxTitle: { fontWeight: 600, marginBottom: 4, color: C.danger },
225
+ diffbox: { border: "1px solid " + C.warnLine, background: C.warnBg, borderRadius: 8, padding: "8px 12px", marginTop: 10, fontSize: fs(FS.sm), lineHeight: 1.6, color: "inherit" },
226
+ diffboxTitle: { fontWeight: 600, marginBottom: 4, color: C.warn },
227
+ diffboxText: { opacity: 0.85, marginBottom: 2 },
228
+ diffboxWarn: { marginTop: 6, color: C.danger, fontWeight: 600 },
229
+ infobox: { border: "1px solid " + C.accentLine, background: C.accentSoft, borderRadius: 8, padding: 12, marginTop: 18, fontSize: fs(FS.md) },
230
+ infoboxTitle: { fontWeight: 600, marginBottom: 4, color: C.accent },
231
+ actionLink: { display: "inline-block", marginTop: 8, color: C.accentText, textDecoration: "underline", fontSize: fs(FS.md) },
232
+ // 官方来源 / 价格依据 / 「为什么显示这个价格」说明块
233
+ officialLinks: { marginTop: 10, paddingTop: 8, borderTop: "1px solid " + C.line },
234
+ officialLinksTitle: { fontWeight: 600, marginBottom: 2 },
235
+ officialLinksHint: { fontSize: fs(FS.sm), opacity: 0.7, marginBottom: 4 },
236
+ officialLinkRow: { lineHeight: 1.95 },
237
+ officialLink: { color: C.accentText, textDecoration: "underline", fontSize: fs(FS.md) },
238
+ whybox: { border: "1px solid " + C.accentLine, background: C.accentSoft, borderRadius: 8, padding: "10px 12px", marginTop: 10, fontSize: fs(FS.sm), lineHeight: 1.7, color: "inherit" },
239
+ whyboxTitle: { fontWeight: 600, marginBottom: 4, color: C.accent },
240
+ whyboxText: { opacity: 0.9, marginBottom: 4 },
241
+ whyboxTip: { opacity: 0.95, marginTop: 2, fontWeight: 600, color: C.accent },
242
+ hero: { borderRadius: 12, padding: 20, color: G.inkOnAccent, background: G.brand, display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 6 },
243
+ heroLabel: { fontSize: fs(FS.md), opacity: 0.85 },
244
+ heroValue: { fontSize: fs(FS.display), fontWeight: 700, letterSpacing: 0.5 },
245
+ heroCurrency: { fontSize: fs(FS.md), opacity: 0.9 },
246
+ // ── 百炼 Token Plan 纯进度条样式(白色填充,蓝色 hero 高对比)──
247
+ qwenHero: { display: "flex", flexDirection: "column", alignItems: "stretch", gap: 10, width: "100%" },
248
+ qwenBarWrap: { width: "100%" },
249
+ qwenBar: { width: "100%", height: 16, borderRadius: 999, background: "rgba(0,0,0,.22)", overflow: "hidden", position: "relative", boxShadow: "inset 0 1px 2px rgba(0,0,0,.25)" },
250
+ qwenBarFill: { height: "100%", borderRadius: 999, background: G.onAccent, transition: "width .4s ease", boxShadow: "0 0 6px rgba(255,255,255,.35)" },
251
+ qwenHeroTitle: { fontSize: fs(FS.xl), fontWeight: 600, opacity: 0.95 },
252
+ qwenHeroUsed: { fontSize: fs(FS.xl), fontWeight: 700, textAlign: "center", letterSpacing: 0.5 },
253
+ // ── 本周配额卡片:连续线段 + 今天节点 ──
254
+ qwenWeekCard: { border: "1px solid rgba(90,140,255,.25)", background: "rgba(90,140,255,.06)", borderRadius: 10, padding: "14px 16px", marginTop: 14 },
255
+ qwenWeekLabel: { fontSize: fs(FS.sm), opacity: 0.6, marginBottom: 8, fontWeight: 600 },
256
+ qwenWeekRow: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, flexWrap: "nowrap" },
257
+ qwenWeekItem: { display: "flex", flexDirection: "column", gap: 2, flexShrink: 0 },
258
+ qwenWeekItemR: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 2, flexShrink: 0 },
259
+ qwenWeekHint: { fontSize: fs(FS.xs), opacity: 0.55 },
260
+ qwenWeekVal: { fontSize: fs(FS.lg), fontWeight: 600 },
261
+ qwenWeekLine: { position: "relative", flex: "1 1 auto", minWidth: 80, height: 24, display: "flex", alignItems: "center", margin: "0 4px" },
262
+ qwenWeekTrack: { position: "relative", width: "100%", height: 5, borderRadius: 999, background: C.accentBg },
263
+ qwenWeekProgress: { position: "absolute", left: 0, top: 0, bottom: 0, width: "0%", borderRadius: 999, background: "linear-gradient(90deg," + C.accentText + "," + C.accent + ")", boxShadow: "0 0 6px " + C.accentLine, transition: "width .4s ease" },
264
+ // 「今天」节点:固定深色小胶囊,与宿主图表底色无关,故不走主题色
265
+ qwenWeekToday: { position: "absolute", top: "50%", transform: "translate(-50%,-50%)", display: "flex", flexDirection: "column", alignItems: "center", gap: 3, background: G.chipDark, padding: "3px 8px", borderRadius: 999, border: "1px solid " + C.accentLine, whiteSpace: "nowrap", zIndex: 2, transition: "left .4s ease" },
266
+ qwenWeekTodayDot: { width: 7, height: 7, borderRadius: "50%", background: C.accent, boxShadow: "0 0 6px " + C.accentLine },
267
+ qwenWeekTodayDate: { fontSize: fs(FS.sm), fontWeight: 600, color: C.accent, lineHeight: 1 },
268
+ calBar: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", marginTop: 10 },
269
+ calGrid: { display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 4, marginTop: 6 },
270
+ calWk: { textAlign: "center", fontSize: fs(FS.sm), opacity: 0.5, padding: "2px 0" },
271
+ calCell: { border: "1px solid rgba(128,128,128,.15)", borderRadius: 6, padding: "6px 2px", textAlign: "center", fontSize: fs(FS.md), cursor: "pointer", minHeight: 40, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 2 },
272
+ calCellBlank: { border: "1px solid transparent", padding: "6px 2px", minHeight: 40 },
273
+ calCellOn: { outline: "2px solid " + C.accent, outlineOffset: -2 },
274
+ calCellVal: { fontSize: fs(FS.xs), opacity: 0.8, maxWidth: "100%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
275
+ legend: { display: "flex", alignItems: "center", gap: 6, fontSize: fs(FS.sm), opacity: 0.65, marginTop: 8, flexWrap: "wrap" },
276
+ legendBar: { display: "inline-block", width: 100, height: 8, borderRadius: 4, background: "linear-gradient(90deg,rgba(128,128,128,.12),rgba(46,134,222,.3),rgba(46,134,222,.85))" },
277
+ numInput: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 4, padding: "3px 6px", fontSize: fs(FS.md), color: "inherit", width: 78, textAlign: "right" },
278
+ selDayTitle: { fontSize: fs(FS.lg), fontWeight: 600, marginTop: 14 },
279
+ billHint: { border: "1px solid " + C.accentLine, background: C.accentSoft, borderRadius: 8, padding: "10px 14px", marginTop: 10, fontSize: fs(FS.md), lineHeight: 1.9 },
280
+ billHintTitle: { fontWeight: 600, marginBottom: 4, color: C.accent },
281
+ nowPeriod: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", marginTop: 10, fontSize: fs(FS.md) },
282
+ loading: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, padding: "48px 0", color: "inherit" },
283
+ spinner: { width: 28, height: 28, borderRadius: "50%", border: "3px solid " + C.accentBg, borderTopColor: C.accent, animation: "dshUsageSpin .8s linear infinite", flexShrink: 0 },
284
+ loadText: { fontSize: fs(FS.md), opacity: 0.65 },
285
+ // 预算进度条(摘要条内使用)
286
+ budgetHero: { flex: "1 1 220px", minWidth: 200 },
287
+ budgetHeroLabel: { fontSize: fs(FS.sm), opacity: 0.9, marginBottom: 4, display: "flex", justifyContent: "space-between", gap: 8 },
288
+ 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)" },
289
+ budgetHeroFill: { height: "100%", borderRadius: 999, background: G.onAccent, transition: "width .4s ease" },
290
+ budgetHeroFillWarn: { height: "100%", borderRadius: 999, background: G.budgetWarn, transition: "width .4s ease" },
291
+ budgetHeroFillDanger: { height: "100%", borderRadius: 999, background: G.budgetDanger, transition: "width .4s ease" },
292
+ budgetHeroText: { fontSize: fs(FS.xs), opacity: 0.95, marginTop: 4 },
293
+ // 卡片分两级:普通卡(次要指标)与主卡(关键结论,蓝色边框 + 淡底)
294
+ cardPrimary: { border: "1px solid " + C.accentLine, background: C.accentSoft, borderRadius: 8, padding: "10px 12px", minWidth: 0 },
295
+ // 图表区
296
+ chartGrid: { display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(340px,1fr))", gap: 12, marginTop: 12 },
297
+ chartPanel: { border: "1px solid rgba(128,128,128,.18)", borderRadius: 10, padding: "10px 12px", minWidth: 0 },
298
+ chartPanelTitle: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, fontSize: fs(FS.md), fontWeight: 600, marginBottom: 6 },
299
+ chartCanvas: { width: "100%", display: "block" },
300
+ legendRow: { display: "flex", alignItems: "center", gap: 8, fontSize: fs(FS.sm), opacity: 0.8, flexWrap: "wrap", marginTop: 6 },
301
+ legendDot: { display: "inline-block", width: 10, height: 10, borderRadius: 3 },
302
+ // 环比徽章(涨/跌):涨用 danger、跌用 ok,与"变差/变好"的语义一致
303
+ badgeUp: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(FS.sm), fontWeight: 500, background: C.dangerBg, color: C.danger, whiteSpace: "nowrap" },
304
+ badgeDown: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(FS.sm), fontWeight: 500, background: C.okBg, color: C.ok, whiteSpace: "nowrap" },
305
+ // 会话排行
306
+ sessionCard: { border: "1px solid rgba(128,128,128,.18)", borderRadius: 10, padding: "10px 12px", marginTop: 12 },
307
+ sessionRow: { display: "flex", alignItems: "center", gap: 10, padding: "6px 0", borderBottom: "1px solid rgba(128,128,128,.12)", fontSize: fs(FS.md) },
308
+ sessionName: { flex: "1 1 0", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
309
+ sessionBar: { flex: "1 1 120px", height: 8, borderRadius: 999, background: "rgba(128,128,128,.15)", overflow: "hidden", minWidth: 80 },
310
+ sessionBarFill: { height: "100%", borderRadius: 999, background: "linear-gradient(90deg," + C.accentText + "," + C.accent + ")" },
311
+ sessionCost: { textAlign: "right", whiteSpace: "nowrap", fontWeight: 600, minWidth: 74 },
312
+ sessionSub: { fontSize: fs(FS.xs), opacity: 0.55, whiteSpace: "nowrap" },
313
+ // 详细数据区(分组标题,更醒目 + 副标题说明)
314
+ dataSection: { marginTop: 18, paddingTop: 14, borderTop: "1px solid rgba(128,128,128,.18)" },
315
+ dataSectionTitle: { display: "flex", alignItems: "baseline", gap: 8, fontSize: fs(FS.lg), fontWeight: 700, marginBottom: 2, color: "inherit" },
316
+ dataSectionHint: { fontSize: fs(FS.sm), opacity: 0.55, marginBottom: 8 },
317
+ // 表格行:合计行更突出
318
+ tdTotal: { textAlign: "right", padding: "8px 8px", fontWeight: 700, borderTop: "2px solid " + C.accentLine, background: C.accentSoft, whiteSpace: "normal", wordBreak: "break-word" },
319
+ tdTotalFirst: { textAlign: "left", padding: "8px 8px", fontWeight: 700, borderTop: "2px solid " + C.accentLine, background: C.accentSoft, whiteSpace: "normal", wordBreak: "break-word" },
320
+ // 模型名 / API 服务商列:更宽 + 优雅换行
321
+ 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 },
322
+ 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(FS.sm) }
323
+ };
324
+
325
+ // 注入全局样式(幂等,仅一次):加载动画关键帧等
326
+ if (typeof document !== "undefined" && document.head && !document.getElementById("dsh-usage-plugin-style")) {
327
+ var styleSheetEl = document.createElement("style");
328
+ styleSheetEl.id = "dsh-usage-plugin-style";
329
+ styleSheetEl.textContent = "@keyframes dshUsageSpin{to{transform:rotate(360deg)}}" +
330
+ // 关键修复:本面板常被停靠在很窄的侧栏里(实测约 590px),而宿主窗口可能
331
+ // 有 1100px+。此前的适配用的是媒体查询(按**窗口**宽度),于是窗口够宽、
332
+ // 面板很窄时一条都不生效——明细表 11 列被直接裁掉,「总消耗」看不见。
333
+ // 现在改用**容器查询**,按面板自身宽度逐级折叠次要列。
334
+ ".dsh-usage-scope{container-type:inline-size}" +
335
+ ".dsh-usage-scope *::-webkit-scrollbar{height:8px;width:8px}" +
336
+ ".dsh-usage-scope *::-webkit-scrollbar-thumb{background:rgba(128,128,128,.35);border-radius:4px}" +
337
+ ".dsh-usage-table{min-width:720px}" +
338
+ // 面板 < 860:先去掉「推理」(信息量最低、且是 outputTokens 的子集)
339
+ "@container (max-width:860px){.dsh-usage-table.collapse-mobile tr>*:nth-child(8){display:none!important}}" +
340
+ // 面板 < 720:再去掉「高峰消耗 / 空闲消耗」,只保留「总消耗」;表格贴合容器宽度
341
+ "@container (max-width:720px){.dsh-usage-table.collapse-mobile tr>*:nth-child(9),.dsh-usage-table.collapse-mobile tr>*:nth-child(10){display:none!important}.dsh-usage-table{min-width:0!important;width:100%!important;max-width:100%!important}}" +
342
+ // 面板 < 560:只保留前 3 个标识列 + 最后合计列(模型/服务商/调用/总消耗)
343
+ "@container (max-width:560px){.dsh-usage-table.collapse-mobile tr>*:not(:nth-child(-n+3)):not(:last-child){display:none!important}}" +
344
+ // 老引擎兜底(不支持容器查询时按窗口宽度折叠)
345
+ "@media (max-width:900px){.dsh-usage-table{min-width:0!important;width:100%!important;max-width:100%!important}}" +
346
+ "@media (max-width:540px){.dsh-usage-table.collapse-mobile tr>*:not(:nth-child(-n+3)):not(:last-child){display:none!important}}";
347
+ document.head.appendChild(styleSheetEl);
348
+ }
349
+
350
+ // ── helpers ──
351
+ // canvas 图表颜色适配宿主主题:读取一个隐藏 probe 元素的继承文字颜色,
352
+ // 判断当前是浅色还是深色背景,从而选择可读的文字/网格颜色。
353
+ var isLightBackground = function () {
354
+ try {
355
+ var probe = document.createElement("span");
356
+ probe.style.cssText = "position:absolute;visibility:hidden;pointer-events:none;font-size:10px";
357
+ probe.textContent = "x";
358
+ if (document.body) document.body.appendChild(probe);
359
+ var c = document.body ? window.getComputedStyle(probe).color : "";
360
+ if (document.body && probe.parentNode) probe.parentNode.removeChild(probe);
361
+ var m = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(c || "");
362
+ if (m) {
363
+ var luma = 0.299 * +m[1] + 0.587 * +m[2] + 0.114 * +m[3];
364
+ return luma < 128;
365
+ }
366
+ } catch (e) {}
367
+ try { return !(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches); } catch (e2) {}
368
+ return true;
369
+ };
370
+ var pad2 = function (n) { return (n < 10 ? "0" : "") + n; };
371
+ var fmtInt = function (n) { return String(Math.round(n || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, ","); };
372
+ 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()); };
373
+ var fmtMoney = function (n) { if (!n) return "¥0.0000"; if (n < 0.0001) return "¥" + n.toExponential(2); if (n < 1) return "¥" + n.toFixed(4); return "¥" + n.toFixed(2); };
374
+ var fmtPrice = function (n) { if (n == null || isNaN(n)) return "—"; return String(parseFloat(Number(n).toFixed(4))); };
375
+ var modelLabel = function (mk) { if (mk === "deepseek-flash") return "deepseek-flash"; if (mk === "deepseek-v4-pro") return "deepseek-v4-pro"; return t("未知模型"); };
376
+ // 模型显示名:以请求参数里的真实模型名为准(非 DeepSeek 模型也如实显示),
377
+ // 空模型名时退回已知档位名 / provider / 未知。
378
+ var modelName = function (r) {
379
+ var m = String((r && r.model) || "").trim();
380
+ if (m) return m;
381
+ var mv = r && r.modelKey;
382
+ if (mv && mv !== "unknown") return modelLabel(mv);
383
+ var p = String((r && r.provider) || "").trim();
384
+ return p ? p : t("未知模型");
385
+ };
386
+ // 模型分组键:真实模型名小写(用于把同名的记录归为一组展示)
387
+ var modelGroupKey = function (r) {
388
+ var m = String((r && r.model) || "").trim();
389
+ if (m) return m.toLowerCase();
390
+ var mv = r && r.modelKey;
391
+ if (mv && mv !== "unknown") return mv;
392
+ var p = String((r && r.provider) || "").trim();
393
+ return p ? p.toLowerCase() : "unknown";
394
+ };
395
+ // API 服务商显示名 / 分组键
396
+ var providerName = function (p) {
397
+ var s = String(p == null ? "" : p).trim();
398
+ return s || t("未知服务商");
399
+ };
400
+ var providerGroupKey = function (p) {
401
+ var s = String(p == null ? "" : p).trim();
402
+ return s ? s.toLowerCase() : "unknown";
403
+ };
404
+ var finishLabel = function (f) { if (f === "stop") return t("完成"); if (f === "tool-calls") return t("工具调用"); if (f === "max-tokens") return t("超长"); if (f === "error") return t("错误"); if (f === "aborted") return t("已中断"); if (f === "timeout") return t("超时"); return f || "—"; };
405
+ var pct = function (a, b) { return b > 0 ? (a / b * 100).toFixed(1) + "%" : "—"; };
406
+ // 周末统一按空闲价规则生效时间:北京时间 2026-08-23 00:00(周日)。此前(含周末)仍按原峰谷分段。
407
+ var WEEKEND_FLAT_AT = Date.parse("2026-08-23T00:00:00+08:00");
408
+ var isPeakNow = function (ts) {
409
+ var d = new Date(ts + 8 * 3600 * 1000);
410
+ var wd = d.getUTCDay(); // 0=周日 1=周一 … 6=周六
411
+ if (ts >= WEEKEND_FLAT_AT && (wd === 0 || wd === 6)) return false;
412
+ var t = d.getUTCHours() * 60 + d.getUTCMinutes();
413
+ return (t >= 9 * 60 && t < 12 * 60) || (t >= 14 * 60 && t < 18 * 60);
414
+ };
415
+ // 返回当前计费时段 { peak, label, desc },区分:工作日高峰 / 工作日空闲 / 周末空闲(周末自 2026-08-23 起全天按空闲价)
416
+ var periodNow = function (ts) {
417
+ ts = ts || Date.now();
418
+ var d = new Date(ts + 8 * 3600 * 1000);
419
+ var wd = d.getUTCDay(); // 0=周日 1=周一 … 6=周六
420
+ var isWeekend = wd === 0 || wd === 6;
421
+ var t = d.getUTCHours() * 60 + d.getUTCMinutes();
422
+ var inPeakHours = (t >= 9 * 60 && t < 12 * 60) || (t >= 14 * 60 && t < 18 * 60);
423
+ if (isWeekend) {
424
+ if (ts >= WEEKEND_FLAT_AT) return { peak: false, label: "周末空闲时段", desc: "周末(周六、周日)全天按空闲价计费" };
425
+ return inPeakHours
426
+ ? { peak: true, label: "周末高峰时段", desc: "2026-08-23 前仍按原规则分峰谷" }
427
+ : { peak: false, label: "周末空闲时段", desc: "2026-08-23 前仍按原规则分峰谷" };
428
+ }
429
+ return inPeakHours
430
+ ? { peak: true, label: "工作日高峰时段", desc: "周一至周五 9:00–12:00、14:00–18:00" }
431
+ : { peak: false, label: "工作日空闲时段", desc: "工作日其余时间" };
432
+ };
433
+ // 计费档位选择:auto 用 autoCost(按生效日期自动切换),base 用 baseCost,其余用 peakValleyCost
434
+ // 计费档位显示文案
435
+ var regimeLabel = function (regime) {
436
+ if (regime === "auto") return t("自动(生效前基础价 · 生效后峰谷价)");
437
+ if (regime === "base") return t("基础价格");
438
+ return t("峰谷价格");
439
+ };
440
+ var costHint = function (regime) {
441
+ if (regime === "auto") return t("自动");
442
+ if (regime === "base") return t("基础价格");
443
+ return t("峰谷价格");
444
+ };
445
+ var costOf = function (r, regime) {
446
+ if (!r) return 0;
447
+ if (regime === "base") return r.baseCost || 0;
448
+ if (regime === "auto") return r.autoCost != null ? r.autoCost : (r.baseCost || 0);
449
+ return r.peakValleyCost || 0;
450
+ };
451
+ // 一组记录的消耗按高峰/空闲时段拆分(regime 决定用哪个档位的 cost)
452
+ var splitTotals = function (list, regime) {
453
+ var peak = 0, off = 0;
454
+ for (var i = 0; i < list.length; i++) {
455
+ var c = costOf(list[i], regime);
456
+ if (list[i].peak) peak += c; else off += c;
457
+ }
458
+ return { peak: peak, off: off };
459
+ };
460
+ // 单日高峰/空闲消耗:优先取 host 汇总的 days 字段;缺失(旧 host)时用按记录算好的 fallback
461
+ var daySplit = function (d, regime, fallback) {
462
+ var peak, off;
463
+ if (d) {
464
+ if (regime === "base") { peak = d.basePeakCost; off = d.baseOffPeakCost; }
465
+ else if (regime === "peakValley") { peak = d.pvPeakCost; off = d.pvOffPeakCost; }
466
+ else { peak = d.autoPeakCost; off = d.autoOffPeakCost; }
467
+ if (peak != null && off != null) return { peak: peak, off: off };
468
+ }
469
+ if (fallback) return fallback;
470
+ return { peak: 0, off: 0 };
471
+ };
472
+ var bjKey = function (ts) { var d = new Date(ts + 8 * 3600 * 1000); return d.getUTCFullYear() + "-" + pad2(d.getUTCMonth() + 1) + "-" + pad2(d.getUTCDate()); };
473
+ var bjStartMs = function (key) { var p = String(key).split("-"); return Date.UTC(+p[0], +p[1] - 1, +p[2]) - 8 * 3600 * 1000; };
474
+ var __MONTHS_EN = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
475
+ var __MONTHS_FULL_EN = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
476
+ var __WD_ZH = ["一", "二", "三", "四", "五", "六", "日"];
477
+ var __WD_EN = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
478
+ var weekdayLabels = function () { return __LANG === "en" ? __WD_EN : __WD_ZH; };
479
+ var fmtMonthLabel = function (y, m1) { return __LANG === "en" ? __MONTHS_FULL_EN[m1 - 1] + " " + y : y + "年" + m1 + "月"; };
480
+ var dayLabel = function (key) {
481
+ var p = String(key).split("-");
482
+ if (__LANG === "en") return __MONTHS_EN[(+p[1]) - 1] + " " + (+p[2]) + ", " + p[0];
483
+ return p[0] + "年" + (+p[1]) + "月" + (+p[2]) + "日";
484
+ };
485
+ var dailyStatsTitle = function (y, m1) { return __LANG === "en" ? "Daily stats (" + __MONTHS_FULL_EN[m1 - 1] + " " + y + ")" : "每日统计(" + y + "年" + m1 + "月)"; };
486
+ var monthDays = function (y, m) { return new Date(Date.UTC(y, m + 1, 0)).getUTCDate(); };
487
+ // 每月 1 号在「周一起始」的日历里占第几格。
488
+ // 旧实现用 bjStartMs() 取北京零点对应的**时刻**、再 getUTCDay() 读星期:
489
+ // 那个时刻在 UTC 上已经退回到前一天 16:00,于是读出来的是前一天的星期,
490
+ // 整月日期因此集体左移一列(2026-11:1 号是周日,却画在“周六”列)。
491
+ // 星期几属于**日历日期**,直接用 UTC 日期算,不做任何时区平移。
492
+ var monthOffset = function (y, m) { return (new Date(Date.UTC(y, m, 1)).getUTCDay() + 6) % 7; };
493
+ var api = function (payload) {
494
+ return fetch("/usage/api", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).then(function (r) { return r.json(); });
495
+ };
496
+ var PRICE_MODELS = ["deepseek-flash", "deepseek-v4-pro"];
497
+
498
+ // ── Card ──
499
+ // 卡片两级:默认是次要卡;`primary` 用于整页最关键的那一个数字(如总消耗)。
500
+ function Card(props) {
501
+ return el("div", { style: props.primary ? st.cardPrimary : st.card },
502
+ el("div", { style: st.cardL }, props.label),
503
+ el("div", { style: props.primary ? Object.assign({}, st.cardV, { color: C.accentText }) : st.cardV }, props.value),
504
+ props.hint ? el("div", { style: st.cardH }, props.hint) : null
505
+ );
506
+ }
507
+
508
+ // ── 计费时段说明(区分工作日高峰 / 工作日空闲 / 周末全天空闲)──
509
+ function BillingHint(props) {
510
+ return el("div", { style: st.billHint },
511
+ el("div", { style: st.billHintTitle }, t("高峰 / 空闲时段说明")),
512
+ el("div", null,
513
+ el("span", { style: st.badgePeak }, t("工作日高峰")), t(" 周一至周五 9:00–12:00、14:00–18:00")
514
+ ),
515
+ el("div", null,
516
+ el("span", { style: st.badgeValley }, t("空闲时段")), t(" 工作日其余时间,以及周末(周六、周日)全天(自 2026-08-23 起;此前仍按原规则分峰谷)")
517
+ ),
518
+ el("div", null,
519
+ el("span", { style: st.badgePeak }, t("V4.1 调价")), t(" 2026-09-10 12:00(北京时间)起 deepseek-flash 按新版峰谷价计费(deepseek-v4-pro 不变)")
520
+ )
521
+ );
522
+ }
523
+
524
+ // ── 官方来源与价格依据(说明 + 官方外链)────────────────────────────
525
+ // 用户需要知道「价格为什么是这样」,以及去哪里核对官方原文。
526
+ // 全部为官方链接,统一在新标签页打开;本插件不改价、不代理官方计费。
527
+ var OFFICIAL_LINKS = [
528
+ { href: "https://api-docs.deepseek.com/zh-cn/quick_start/pricing", label: "DeepSeek 官方价格页(模型与价格、峰谷时段定义)" },
529
+ { href: "https://mp.weixin.qq.com/s/qg0NU3NNUbp1co2PdkAPAg", label: "DeepSeek V4.1 Flash 发布公告" },
530
+ { href: "https://platform.deepseek.com/usage", label: "DeepSeek 开放平台用量页(官方账单,用于对账)" },
531
+ { href: "https://api-docs.deepseek.com/zh-cn/", label: "DeepSeek 官方 API 文档" },
532
+ { href: "https://github.com/deepseek-ai/deepseek-harness", label: "DeepSeek Harness 官方仓库" }
533
+ ];
534
+
535
+ function OfficialLinks() {
536
+ return el("div", { style: st.officialLinks },
537
+ el("div", { style: st.officialLinksTitle }, t("官方来源与价格依据")),
538
+ el("div", { style: st.officialLinksHint }, t("价格由 DeepSeek 官方公布,本插件只做本地计算、不修改也不代理官方价格;点下面的链接可直接核对官方原文。")),
539
+ OFFICIAL_LINKS.map(function (l, i) {
540
+ return el("div", { key: "official-link-" + i, style: st.officialLinkRow },
541
+ el("a", { style: st.officialLink, href: l.href, target: "_blank", rel: "noreferrer" }, t(l.label))
542
+ );
543
+ })
544
+ );
545
+ }
546
+
547
+ // ── 「为什么显示这个价格」说明(价格表页与帮助抽屉共用)──
548
+ function WhyThisPrice() {
549
+ return el("div", { style: st.whybox },
550
+ el("div", { style: st.whyboxTitle }, t("为什么显示这个价格?")),
551
+ el("div", { style: st.whyboxText }, t("① 官方价格表按生效时间分段:2026-08-17 00:00 起为第一版峰谷价;2026-09-10 12:00(北京时间)起为 DeepSeek V4.1 Flash 调价后的新版(deepseek-flash 单价下调,deepseek-v4-pro 不变)。每次调用按其发生时刻对应的版本计费,历史金额不会因后续调价而改变。")),
552
+ el("div", { style: st.whyboxText }, t("② 模型档位按官方新命名归并:旧名 deepseek-v4-flash、deepseek-v4-flash-vision-exp 仍可调用(官方已路由到 DeepSeek-V4.1-Flash 并按 Flash 单价计费),计费时与 deepseek-flash 合并为同一档位。")),
553
+ el("div", { style: st.whyboxText }, t("③ 峰谷规则:工作日 9:00–12:00、14:00–18:00(北京时间)为高峰,其余时间(含周末全天)为空闲;空闲价 = 高峰价的一半。")),
554
+ el("div", { style: st.whyboxText }, t("④ 只有能与记录中 provider / model 可靠匹配的价格才参与计费,其余按 ¥0 统计(避免用相近模型的价格误算)。")),
555
+ el("div", { style: st.whyboxText }, t("⑤ 本插件按官方公布价格在本地计算,结果仅供参考,最终以 DeepSeek 官方账单为准;产品价格可能变动,请以官方价格页为准。")),
556
+ el("div", { style: st.whyboxTip }, t("建议:若新模型(如 deepseek-flash)的消耗显示为 ¥0,通常是本插件版本过旧——请升级到 1.17.0 或更高版本。")),
557
+ el(OfficialLinks, null)
558
+ );
559
+ }
560
+
561
+ // ── 数据加载动画 ──
562
+ function LoadingView(props) {
563
+ return el("div", { style: st.loading },
564
+ el("div", { style: st.spinner }),
565
+ el("div", { style: st.loadText }, props.text || "正在加载用量数据…")
566
+ );
567
+ }
568
+
569
+ // ── long-image report ──
570
+ function drawReport(canvas, records) {
571
+ records = records.slice();
572
+ records.sort(function (a, b) { return b.time - a.time; }); // newest first
573
+ var totalIn = records.length;
574
+ if (records.length > 2000) records = records.slice(0, 2000); // canvas height guard
575
+ var W = 1520, P = 40, count = records.length;
576
+ var scale = count <= 200 ? 2 : 1;
577
+ var rowH = count <= 200 ? 28 : count <= 800 ? 20 : 14;
578
+ var fBase = count <= 200 ? 12 : count <= 800 ? 11 : 10;
579
+ var headH = count <= 200 ? 26 : count <= 800 ? 20 : 16;
580
+
581
+ var totalHit = 0, totalMiss = 0, totalWrite = 0, totalOut = 0, totalReason = 0, totalCost = 0, totalPeakCost = 0, totalOffCost = 0;
582
+ for (var i = 0; i < count; i++) {
583
+ var r = records[i];
584
+ totalHit += r.cacheReadTokens || 0; totalMiss += r.inputTokens || 0; totalWrite += r.cacheWriteTokens || 0;
585
+ totalOut += r.outputTokens || 0; totalReason += r.reasoningTokens || 0;
586
+ var cst = r.peakValleyCost || 0;
587
+ totalCost += cst;
588
+ if (r.peak) totalPeakCost += cst; else totalOffCost += cst;
589
+ }
590
+ var hitRate = (totalHit + totalMiss + totalWrite) > 0 ? (totalHit / (totalHit + totalMiss + totalWrite) * 100).toFixed(1) + "%" : "—";
591
+ var byModel = {}, byProvider = {}, byProviderModel = {};
592
+ for (var i2 = 0; i2 < count; i2++) {
593
+ var r2 = records[i2];
594
+ var gk2 = modelGroupKey(r2);
595
+ if (!byModel[gk2]) byModel[gk2] = { key: gk2, name: modelName(r2), calls: 0, hit: 0, miss: 0, out: 0, reason: 0, cost: 0, peakCost: 0, offCost: 0, peakCalls: 0, offCalls: 0 };
596
+ var c2v = r2.peakValleyCost || 0;
597
+ byModel[gk2].calls += 1; byModel[gk2].hit += r2.cacheReadTokens || 0; byModel[gk2].miss += r2.inputTokens || 0;
598
+ byModel[gk2].out += r2.outputTokens || 0; byModel[gk2].reason += r2.reasoningTokens || 0; byModel[gk2].cost += c2v;
599
+ var pk2 = providerGroupKey(r2.provider);
600
+ if (!byProvider[pk2]) byProvider[pk2] = { key: pk2, name: providerName(r2.provider), calls: 0, cost: 0, peakCost: 0, offCost: 0, peakCalls: 0, offCalls: 0 };
601
+ byProvider[pk2].calls += 1; byProvider[pk2].cost += c2v;
602
+ var pmk2 = pk2 + "||" + gk2;
603
+ if (!byProviderModel[pmk2]) byProviderModel[pmk2] = { key: pmk2, providerKey: pk2, providerName: providerName(r2.provider), modelKey: gk2, modelName: modelName(r2), calls: 0, cost: 0, peakCost: 0, offCost: 0, peakCalls: 0, offCalls: 0 };
604
+ byProviderModel[pmk2].calls += 1; byProviderModel[pmk2].cost += c2v;
605
+ if (r2.peak) {
606
+ byModel[gk2].peakCost += c2v; byModel[gk2].peakCalls += 1;
607
+ byProvider[pk2].peakCost += c2v; byProvider[pk2].peakCalls += 1;
608
+ byProviderModel[pmk2].peakCost += c2v; byProviderModel[pmk2].peakCalls += 1;
609
+ } else {
610
+ byModel[gk2].offCost += c2v; byModel[gk2].offCalls += 1;
611
+ byProvider[pk2].offCost += c2v; byProvider[pk2].offCalls += 1;
612
+ byProviderModel[pmk2].offCost += c2v; byProviderModel[pmk2].offCalls += 1;
613
+ }
614
+ }
615
+ var modelRows = [];
616
+ for (var k in byModel) modelRows.push(byModel[k]);
617
+ modelRows.sort(function (a, b) { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
618
+ var providerRows = [];
619
+ for (var kp in byProvider) providerRows.push(byProvider[kp]);
620
+ providerRows.sort(function (a, b) { return b.cost - a.cost || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0); });
621
+ var providerGroups = [];
622
+ for (var pg = 0; pg < providerRows.length; pg++) {
623
+ var gprov = providerRows[pg];
624
+ var gmodels = [];
625
+ for (var kgm in byProviderModel) {
626
+ if (byProviderModel[kgm].providerKey === gprov.key) gmodels.push(byProviderModel[kgm]);
627
+ }
628
+ gmodels.sort(function (a, b) { return b.cost - a.cost || (a.modelKey < b.modelKey ? -1 : 1); });
629
+ providerGroups.push({ provider: gprov, models: gmodels });
630
+ }
631
+ // 服务商×模型明细的总行数(组头 + 组内模型 + 合计行)
632
+ var pmRowTotal = 1;
633
+ for (var pgt = 0; pgt < providerGroups.length; pgt++) pmRowTotal += 1 + providerGroups[pgt].models.length;
634
+
635
+ var H = 244 + headH + count * rowH + 24 + 18 + headH + (modelRows.length + 1) * rowH + 24 + 18 + headH + pmRowTotal * rowH + 26 + 44;
636
+ if (H > 30000 && rowH > 12) { rowH = 12; fBase = 10; headH = 14; H = 244 + headH + count * rowH + 24 + 18 + headH + (modelRows.length + 1) * rowH + 24 + 18 + headH + pmRowTotal * rowH + 26 + 44; }
637
+ canvas.width = W * scale;
638
+ canvas.height = H * scale;
639
+ var ctx = canvas.getContext("2d");
640
+ if (!ctx) throw new Error(t("canvas 2d 不可用"));
641
+ ctx.scale(scale, scale);
642
+ ctx.textBaseline = "alphabetic";
643
+ var font = function (size, weight) { ctx.font = (weight || "400") + " " + size + 'px "Segoe UI","Microsoft YaHei",sans-serif'; };
644
+
645
+ ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, W, H);
646
+ ctx.fillStyle = "#3a7bd5"; ctx.fillRect(0, 0, W, 64);
647
+ ctx.fillStyle = "#ffffff"; ctx.textAlign = "left"; font(20, "700");
648
+ ctx.fillText(t("用量与消耗报告"), P, 40);
649
+ ctx.fillStyle = "#7a8699"; font(13, "400");
650
+ ctx.fillText(t("生成时间 ") + fmtTime(Date.now()) + t("(北京) · 共 ") + count + t(" 条调用 · 缓存命中率 ") + hitRate +
651
+ t(" · 高峰价消耗 ") + fmtMoney(totalPeakCost) + t(" · 空闲价消耗 ") + fmtMoney(totalOffCost) + t(" · 合计 ") + fmtMoney(totalCost) +
652
+ (totalIn > count ? t(" · 报告仅含最近 ") + count + t(" 条") : ""), P, 92);
653
+
654
+ var cardW = (W - 2 * P - 6 * 10) / 7;
655
+ var cards = [
656
+ { l: t("调用次数"), v: fmtInt(count) }, { l: t("输入 · 未命中"), v: fmtInt(totalMiss) },
657
+ { l: t("输入 · 缓存命中"), v: fmtInt(totalHit) }, { l: t("输出"), v: fmtInt(totalOut) },
658
+ { l: t("高峰消耗"), v: fmtMoney(totalPeakCost) }, { l: t("空闲消耗"), v: fmtMoney(totalOffCost) },
659
+ { l: t("总消耗 (峰谷价)"), v: fmtMoney(totalCost) }
660
+ ];
661
+ for (var c = 0; c < cards.length; c++) {
662
+ var x = P + c * (cardW + 10);
663
+ ctx.fillStyle = "#f5f7fa"; ctx.fillRect(x, 112, cardW, 70);
664
+ ctx.strokeStyle = "#e4e8ee"; ctx.strokeRect(x, 112, cardW, 70);
665
+ ctx.fillStyle = "#7a8699"; font(12, "400"); ctx.fillText(cards[c].l, x + 14, 138);
666
+ ctx.fillStyle = "#1c2733"; font(19, "600"); ctx.fillText(cards[c].v, x + 14, 166);
667
+ }
668
+
669
+ var c1 = { time: P, model: P + 150, missR: P + 440, hitR: P + 540, writeR: P + 630, outR: P + 720, reasonR: P + 810, rateR: P + 900, periodR: P + 990, endR: P + 1080, costR: P + 1440 };
670
+ var y = 226;
671
+ ctx.fillStyle = "#1c2733"; font(15, "600"); ctx.fillText(t("缓存命中列表(共 ") + count + t(" 条)"), P, y);
672
+ y = 244;
673
+ ctx.fillStyle = "#eef1f5"; ctx.fillRect(P, y, W - 2 * P, headH);
674
+ ctx.fillStyle = "#55617a"; font(fBase + 1, "600");
675
+ var hty = y + Math.round(headH * 0.65);
676
+ ctx.textAlign = "left"; ctx.fillText(t("时间(北京)"), c1.time, hty); ctx.fillText(t("模型"), c1.model, hty);
677
+ ctx.textAlign = "right";
678
+ ctx.fillText(t("输入·未命中"), c1.missR, hty); ctx.fillText(t("缓存命中"), c1.hitR, hty); ctx.fillText(t("缓存写入"), c1.writeR, hty);
679
+ ctx.fillText(t("输出"), c1.outR, hty); ctx.fillText(t("推理"), c1.reasonR, hty); ctx.fillText(t("命中率"), c1.rateR, hty);
680
+ ctx.fillText(t("时段"), c1.periodR, hty); ctx.fillText(t("结束"), c1.endR, hty); ctx.fillText(t("消耗(峰谷)"), c1.costR, hty);
681
+ y += headH;
682
+ for (var r3 = 0; r3 < count; r3++) {
683
+ var rec = records[r3];
684
+ var hit = rec.cacheReadTokens || 0, miss = rec.inputTokens || 0, cost = rec.peakValleyCost || 0;
685
+ var intR = !!rec.interrupted;
686
+ ctx.fillStyle = r3 % 2 === 1 ? "#fafbfc" : "#ffffff"; ctx.fillRect(P, y, W - 2 * P, rowH);
687
+ var ty = y + Math.round(rowH * 0.68);
688
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(fBase, "400");
689
+ ctx.fillText(fmtTime(rec.time), c1.time, ty); ctx.fillText(modelName(rec), c1.model, ty);
690
+ if (intR) { ctx.fillStyle = "#e74c3c"; ctx.fillText(t("中断"), c1.model + ctx.measureText(modelName(rec)).width + 12, ty); ctx.fillStyle = "#1c2733"; }
691
+ ctx.textAlign = "right";
692
+ ctx.fillText(fmtInt(miss), c1.missR, ty);
693
+ ctx.fillStyle = "#22a45d"; ctx.fillText(fmtInt(hit), c1.hitR, ty);
694
+ ctx.fillStyle = "#1c2733";
695
+ // DeepSeek 系 provider 不上报 cacheWriteTokens(官方计费里"缓存写入"本就
696
+ // 按未命中价计、不单列),所以这一列显示 —。此前用 inputTokens 兜底展示,
697
+ // 等于在图上凭空虚增一笔与未命中相等的写入,反而让数字对不上。
698
+ ctx.fillText(rec.cacheWriteTokens ? fmtInt(rec.cacheWriteTokens) : "—", c1.writeR, ty);
699
+ ctx.fillText(fmtInt(rec.outputTokens || 0), c1.outR, ty);
700
+ ctx.fillText(rec.reasoningTokens ? fmtInt(rec.reasoningTokens) : "—", c1.reasonR, ty);
701
+ ctx.fillStyle = "#55617a"; ctx.fillText(pct(hit, hit + miss), c1.rateR, ty);
702
+ ctx.fillStyle = rec.peak ? C.warn : C.accentText; ctx.fillText(rec.peak ? t("峰") : t("谷"), c1.periodR, ty);
703
+ ctx.fillStyle = intR ? "#e74c3c" : "#55617a"; ctx.fillText(finishLabel(rec.finishReason), c1.endR, ty);
704
+ ctx.fillStyle = rec.peak ? C.warn : C.accentText; ctx.fillText(intR ? "—" : fmtMoney(cost), c1.costR, ty);
705
+ y += rowH;
706
+ }
707
+
708
+ y += 24;
709
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(15, "600");
710
+ ctx.fillText(t("消耗表(按模型 · 峰谷价 · 高峰/空闲分列)"), P, y);
711
+ y += 18;
712
+ var c2 = { model: P, callsR: P + 330, missR: P + 470, hitR: P + 590, outR: P + 710, reasonR: P + 840, peakR: P + 1010, offR: P + 1140, costR: P + 1440 };
713
+ ctx.fillStyle = "#eef1f5"; ctx.fillRect(P, y, W - 2 * P, headH);
714
+ ctx.fillStyle = "#55617a"; font(fBase + 1, "600");
715
+ var hty2 = y + Math.round(headH * 0.65);
716
+ ctx.textAlign = "left"; ctx.fillText(t("模型"), c2.model, hty2);
717
+ ctx.textAlign = "right";
718
+ ctx.fillText(t("调用"), c2.callsR, hty2); ctx.fillText(t("输入·未命中"), c2.missR, hty2); ctx.fillText(t("缓存命中"), c2.hitR, hty2);
719
+ ctx.fillText(t("输出"), c2.outR, hty2); ctx.fillText(t("推理"), c2.reasonR, hty2);
720
+ ctx.fillText(t("高峰消耗"), c2.peakR, hty2); ctx.fillText(t("空闲消耗"), c2.offR, hty2); ctx.fillText(t("总消耗"), c2.costR, hty2);
721
+ y += headH;
722
+ var t2rows = modelRows.concat([{ key: "合计", name: t("合计"), calls: count, miss: totalMiss, hit: totalHit, out: totalOut, reason: totalReason, cost: totalCost, peakCost: totalPeakCost, offCost: totalOffCost }]);
723
+ for (var m = 0; m < t2rows.length; m++) {
724
+ var row = t2rows[m];
725
+ var isTotal = row.key === "合计";
726
+ ctx.fillStyle = isTotal ? "#eef1f5" : (m % 2 === 1 ? "#fafbfc" : "#ffffff"); ctx.fillRect(P, y, W - 2 * P, rowH);
727
+ var ty2 = y + Math.round(rowH * 0.68);
728
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(fBase, isTotal ? "600" : "400");
729
+ ctx.fillText(row.name || row.key, c2.model, ty2);
730
+ ctx.textAlign = "right";
731
+ ctx.fillText(fmtInt(row.calls), c2.callsR, ty2); ctx.fillText(fmtInt(row.miss), c2.missR, ty2);
732
+ ctx.fillStyle = "#22a45d"; ctx.fillText(fmtInt(row.hit), c2.hitR, ty2);
733
+ ctx.fillStyle = "#1c2733"; font(fBase, isTotal ? "600" : "400");
734
+ ctx.fillText(fmtInt(row.out), c2.outR, ty2); ctx.fillText(row.reason ? fmtInt(row.reason) : "—", c2.reasonR, ty2);
735
+ ctx.fillStyle = "#e08700"; ctx.fillText(fmtMoney(row.peakCost), c2.peakR, ty2);
736
+ ctx.fillStyle = C.accentText; ctx.fillText(fmtMoney(row.offCost), c2.offR, ty2);
737
+ ctx.fillStyle = "#1c2733"; ctx.fillText(fmtMoney(row.cost), c2.costR, ty2);
738
+ y += rowH;
739
+ }
740
+ y += 24;
741
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(15, "600");
742
+ ctx.fillText(t("消耗表(按 API 服务商 × 模型 · 峰谷价)"), P, y);
743
+ y += 18;
744
+ ctx.fillStyle = "#eef1f5"; ctx.fillRect(P, y, W - 2 * P, headH);
745
+ ctx.fillStyle = "#55617a"; font(fBase + 1, "600");
746
+ var hty3 = y + Math.round(headH * 0.65);
747
+ ctx.textAlign = "left"; ctx.fillText(t("API 服务商 / 模型"), c2.model, hty3);
748
+ ctx.textAlign = "right";
749
+ ctx.fillText(t("调用"), c2.callsR, hty3);
750
+ ctx.fillText(t("高峰消耗"), c2.peakR, hty3); ctx.fillText(t("空闲消耗"), c2.offR, hty3); ctx.fillText(t("总消耗"), c2.costR, hty3);
751
+ y += headH;
752
+ var p3n = 0;
753
+ for (var p3g = 0; p3g < providerGroups.length; p3g++) {
754
+ var pgrp = providerGroups[p3g];
755
+ var gpr = pgrp.provider;
756
+ ctx.fillStyle = "#e8edf5"; ctx.fillRect(P, y, W - 2 * P, rowH);
757
+ var ty3 = y + Math.round(rowH * 0.68);
758
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(fBase, "600");
759
+ ctx.fillText(gpr.name + "(" + pgrp.models.length + t(" 个模型)"), c2.model, ty3);
760
+ ctx.textAlign = "right"; ctx.fillStyle = "#1c2733"; font(fBase, "600");
761
+ ctx.fillText(fmtInt(gpr.calls), c2.callsR, ty3);
762
+ ctx.fillStyle = "#e08700"; ctx.fillText(fmtMoney(gpr.peakCost), c2.peakR, ty3);
763
+ ctx.fillStyle = C.accentText; ctx.fillText(fmtMoney(gpr.offCost), c2.offR, ty3);
764
+ ctx.fillStyle = "#1c2733"; ctx.fillText(fmtMoney(gpr.cost), c2.costR, ty3);
765
+ y += rowH;
766
+ for (var p3m = 0; p3m < pgrp.models.length; p3m++) {
767
+ var pmd = pgrp.models[p3m];
768
+ ctx.fillStyle = (p3n++ % 2 === 1) ? "#fafbfc" : "#ffffff"; ctx.fillRect(P, y, W - 2 * P, rowH);
769
+ var ty4 = y + Math.round(rowH * 0.68);
770
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(fBase, "400");
771
+ ctx.fillText(" " + pmd.modelName, c2.model, ty4);
772
+ ctx.textAlign = "right"; ctx.fillStyle = "#1c2733"; font(fBase, "400");
773
+ ctx.fillText(fmtInt(pmd.calls), c2.callsR, ty4);
774
+ ctx.fillStyle = "#e08700"; ctx.fillText(fmtMoney(pmd.peakCost), c2.peakR, ty4);
775
+ ctx.fillStyle = C.accentText; ctx.fillText(fmtMoney(pmd.offCost), c2.offR, ty4);
776
+ ctx.fillStyle = "#1c2733"; ctx.fillText(fmtMoney(pmd.cost), c2.costR, ty4);
777
+ y += rowH;
778
+ }
779
+ }
780
+ ctx.fillStyle = "#eef1f5"; ctx.fillRect(P, y, W - 2 * P, rowH);
781
+ var ty5 = y + Math.round(rowH * 0.68);
782
+ ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(fBase, "600");
783
+ ctx.fillText(t("总费用合计"), c2.model, ty5);
784
+ ctx.textAlign = "right"; ctx.fillStyle = "#1c2733"; font(fBase, "600");
785
+ ctx.fillText(fmtInt(count), c2.callsR, ty5);
786
+ ctx.fillStyle = "#e08700"; ctx.fillText(fmtMoney(totalPeakCost), c2.peakR, ty5);
787
+ ctx.fillStyle = C.accentText; ctx.fillText(fmtMoney(totalOffCost), c2.offR, ty5);
788
+ ctx.fillStyle = "#1c2733"; ctx.fillText(fmtMoney(totalCost), c2.costR, ty5);
789
+ y += rowH;
790
+ y += 26;
791
+ ctx.textAlign = "left"; ctx.fillStyle = "#98a2b3"; font(11, "400");
792
+ ctx.fillText(t("计价按请求的 API 服务商分别应用已核验价格;DigitalOcean 美元价按 USD/CNY 汇率折算;无法可靠映射的第三方价格按 ¥0。"), P, y);
793
+ }
794
+
795
+ /**
796
+ * 趋势图 X 轴日期刻度排布(采纳自 PR #18,@vb2250158)。
797
+ *
798
+ * 旧实现按固定步长 `ceil(series.length / 8)` 取日期,只看数量、不看文字宽度:
799
+ * 30 天视图在窄面板里最后两个日期会叠在一起(实测 10/6 与 10/7 重叠约 16px)。
800
+ * 现在按 canvas 实测文本宽度做贪心排布,并优先保证**首尾两个日期**可见;
801
+ * 两者确实放不下时只保留最近的日期。
802
+ *
803
+ * @param series - 时间正序的系列(每项含 key: 'YYYY-MM-DD')。
804
+ * @param x0 - 第一根柱子的左边缘。
805
+ * @param gap - 柱间距(柱宽 + 空隙)。
806
+ * @param bw - 柱宽。
807
+ * @param left - 绘图区左边界。
808
+ * @param right - 绘图区右边界。
809
+ * @param measure - 文本宽度测量函数(通常 `ctx.measureText(t).width`)。
810
+ * @returns 需要绘制的刻度数组(按 x 升序)。
811
+ */
812
+ function trendDateTicks(series, x0, gap, bw, left, right, measure) {
813
+ var n = series.length;
814
+ if (!n) return [];
815
+ var SPACING = 8;
816
+ function labelAt(i) {
817
+ var dp = String(series[i].key).split("-");
818
+ return (+dp[1]) + "/" + (+dp[2]);
819
+ }
820
+ function tickAt(i) {
821
+ var text = labelAt(i);
822
+ var w = measure(text);
823
+ var cx = x0 + i * gap + bw / 2;
824
+ var x = Math.min(Math.max(cx, left + w / 2), right - w / 2);
825
+ return { index: i, text: text, x: x, left: x - w / 2, right: x + w / 2 };
826
+ }
827
+ if (n === 1) return [tickAt(0)];
828
+ var first = tickAt(0);
829
+ var last = tickAt(n - 1);
830
+ // 首尾都放不下:只保留最近的日期,避免画出一团重叠文字
831
+ if (last.left - first.right < SPACING) return [last];
832
+ var stride = Math.max(1, Math.ceil(n / 8));
833
+ var out = [first];
834
+ var prevRight = first.right;
835
+ for (var i = stride; i < n - 1; i += stride) {
836
+ var cand = tickAt(i);
837
+ if (cand.right + SPACING > last.left) break; // 不能压到最后一个日期
838
+ if (cand.left < prevRight + SPACING) continue; // 与上一个太近
839
+ out.push(cand);
840
+ prevRight = cand.right;
841
+ }
842
+ out.push(last);
843
+ return out;
844
+ }
845
+
846
+ // ── 近 30 天消耗趋势图(canvas 堆叠柱状:高峰橙 + 空闲蓝)──
847
+ var TREND_COLORS = { peak: C.warn, off: C.accent };
848
+ function TrendChart(props) {
849
+ var days = props.days || [];
850
+ var canvasRef = React.useRef(null);
851
+ var wrapRef = React.useRef(null);
852
+ var tipState = React.useState(null);
853
+ var tip = tipState[0], setTip = tipState[1];
854
+ var rangeState = React.useState(props.range || 30);
855
+ var range = rangeState[0], setRange = rangeState[1];
856
+ // 组装最近 N 天(含无记录日),时间正序
857
+ function buildSeries() {
858
+ var N = range, map = {}, out = [];
859
+ for (var i = 0; i < days.length; i++) map[days[i].day] = days[i];
860
+ var nowKey = bjKey(Date.now());
861
+ var p = nowKey.split("-");
862
+ var base = Date.UTC(+p[0], +p[1] - 1, +p[2]) - 8 * 3600 * 1000;
863
+ for (var k = N - 1; k >= 0; k--) {
864
+ var ms = base - k * 86400000;
865
+ var d = new Date(ms + 8 * 3600 * 1000);
866
+ var key = d.getUTCFullYear() + "-" + pad2(d.getUTCMonth() + 1) + "-" + pad2(d.getUTCDate());
867
+ var rec = map[key];
868
+ var split = daySplit(rec, props.regime || "auto");
869
+ out.push({ key: key, peak: split.peak, off: split.off, total: split.peak + split.off, calls: rec ? (rec.calls || 0) : 0 });
870
+ }
871
+ return out;
872
+ }
873
+ function redraw() {
874
+ var node = canvasRef.current, wrap = wrapRef.current;
875
+ if (!node || !wrap) return;
876
+ var series = buildSeries();
877
+ var cssW = wrap.clientWidth || 600, cssH = 190;
878
+ var dpr = (typeof window !== "undefined" && window.devicePixelRatio) || 1;
879
+ node.width = Math.round(cssW * dpr);
880
+ node.height = Math.round(cssH * dpr);
881
+ node.style.width = cssW + "px";
882
+ node.style.height = cssH + "px";
883
+ var ctx = node.getContext("2d");
884
+ if (!ctx) return;
885
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
886
+ ctx.clearRect(0, 0, cssW, cssH);
887
+ var lightBg = isLightBackground();
888
+ var inkMuted = lightBg ? "rgba(128,128,128,.6)" : "rgba(215,222,232,.6)";
889
+ var gridColor = lightBg ? "rgba(128,128,128,.15)" : "rgba(255,255,255,.15)";
890
+ var emptyColor = lightBg ? "rgba(128,128,128,.5)" : "rgba(215,222,232,.55)";
891
+ var padL = 54, padR = 10, padT = 18, padB = 24;
892
+ var chartW = cssW - padL - padR, chartH = cssH - padT - padB;
893
+ var maxV = 0;
894
+ for (var i = 0; i < series.length; i++) if (series[i].total > maxV) maxV = series[i].total;
895
+ if (maxV <= 0) {
896
+ ctx.fillStyle = emptyColor;
897
+ ctx.font = "12px sans-serif";
898
+ ctx.textAlign = "center";
899
+ ctx.fillText(t("暂无数据"), cssW / 2, cssH / 2);
900
+ node.__series = null;
901
+ return;
902
+ }
903
+ ctx.strokeStyle = gridColor;
904
+ ctx.lineWidth = 1;
905
+ ctx.fillStyle = inkMuted;
906
+ ctx.font = "10px sans-serif";
907
+ ctx.textAlign = "right";
908
+ for (var g = 0; g <= 4; g++) {
909
+ var gy = padT + chartH - (chartH * g / 4);
910
+ ctx.beginPath(); ctx.moveTo(padL, gy); ctx.lineTo(cssW - padR, gy); ctx.stroke();
911
+ ctx.fillText(fmtMoney(maxV * g / 4).replace("¥", ""), padL - 6, gy + 3);
912
+ }
913
+ var bw = Math.max(1, Math.min(14, (chartW - 6) / series.length - 2));
914
+ var gap = bw + 2, totalW = gap * series.length - 2, x0 = padL + (chartW - totalW) / 2;
915
+ for (var s = 0; s < series.length; s++) {
916
+ var it = series[s], x = x0 + s * gap;
917
+ var offH = it.off > 0 ? (it.off / maxV * chartH) : 0;
918
+ var peakH = it.peak > 0 ? (it.peak / maxV * chartH) : 0;
919
+ var yBottom = padT + chartH;
920
+ if (offH > 0) { ctx.fillStyle = TREND_COLORS.off; ctx.fillRect(x, yBottom - offH, bw, offH); }
921
+ if (peakH > 0) { ctx.fillStyle = TREND_COLORS.peak; ctx.fillRect(x, yBottom - offH - peakH, bw, peakH); }
922
+ }
923
+ // 日期刻度:按实测文字宽度排布,避免窄图下最后两个日期重叠
924
+ ctx.font = "10px sans-serif";
925
+ var ticks = trendDateTicks(series, x0, gap, bw, padL, cssW - padR, function (text) { return ctx.measureText(text).width; });
926
+ ctx.fillStyle = inkMuted;
927
+ ctx.textAlign = "center";
928
+ for (var ti = 0; ti < ticks.length; ti++) ctx.fillText(ticks[ti].text, ticks[ti].x, cssH - 6);
929
+ node.__series = series; node.__x0 = x0; node.__gap = gap; node.__bw = bw; node.__padT = padT; node.__chartH = chartH;
930
+ }
931
+ React.useEffect(function () {
932
+ redraw();
933
+ function onResize() { redraw(); }
934
+ if (typeof window !== "undefined") window.addEventListener("resize", onResize);
935
+ return function () { if (typeof window !== "undefined") window.removeEventListener("resize", onResize); };
936
+ }, [days, props.regime, range]);
937
+ function onMove(e) {
938
+ var node = canvasRef.current;
939
+ if (!node || !node.__series) return;
940
+ var rect = node.getBoundingClientRect();
941
+ var idx = Math.floor((e.clientX - rect.left - node.__x0) / node.__gap);
942
+ if (idx < 0 || idx >= node.__series.length) { setTip(null); return; }
943
+ var it = node.__series[idx];
944
+ 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(" 次") });
945
+ }
946
+ return el("div", { style: { minWidth: 0 } },
947
+ el("div", { style: st.calBar, marginTop: 0 },
948
+ el("div", { style: st.seg },
949
+ [7, 30, 90].map(function (n) {
950
+ return el("button", { key: n, style: range === n ? st.segBtnOn : st.segBtn, onClick: function () { setRange(n); } }, n + t("天"));
951
+ })
952
+ )
953
+ ),
954
+ el("div", { style: { position: "relative", minWidth: 0 }, ref: wrapRef },
955
+ el("canvas", { ref: canvasRef, style: st.chartCanvas, onMouseMove: onMove, onMouseLeave: function () { setTip(null); } }),
956
+ 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(FS.sm), lineHeight: 1.6, whiteSpace: "pre", pointerEvents: "none", zIndex: 5, maxWidth: "92%" } }, tip.text) : null
957
+ )
958
+ );
959
+ }
960
+
961
+ // ── 模型消耗占比环形图(canvas 环形 + HTML 图例)──
962
+ var DONUT_COLORS = [C.accent, C.warn, C.ok, C.danger, "#9b59b6", "#00d2ff", "#e84393", "#fdcb6e", "#6c5ce7", "#00b894"];
963
+ function DonutChart(props) {
964
+ var rows = props.rows || [];
965
+ var canvasRef = React.useRef(null);
966
+ var modeState = React.useState("cost");
967
+ var mode = modeState[0], setMode = modeState[1];
968
+ 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; }
969
+ function draw() {
970
+ var node = canvasRef.current;
971
+ if (!node) return;
972
+ var total = totalVal();
973
+ var cssW = 176, cssH = 176;
974
+ var dpr = (typeof window !== "undefined" && window.devicePixelRatio) || 1;
975
+ node.width = Math.round(cssW * dpr);
976
+ node.height = Math.round(cssH * dpr);
977
+ node.style.width = cssW + "px";
978
+ node.style.height = cssH + "px";
979
+ var ctx = node.getContext("2d");
980
+ if (!ctx) return;
981
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
982
+ ctx.clearRect(0, 0, cssW, cssH);
983
+ var cx = cssW / 2, cy = cssH / 2, r = 68, inner = 44;
984
+ var start = -Math.PI / 2;
985
+ for (var i2 = 0; i2 < rows.length; i2++) {
986
+ var v = mode === "calls" ? (rows[i2].calls || 0) : rows[i2].cost;
987
+ var slice = total > 0 ? (v / total * Math.PI * 2) : (2 * Math.PI / Math.max(rows.length, 1));
988
+ ctx.beginPath();
989
+ ctx.arc(cx, cy, r, start, start + slice);
990
+ ctx.arc(cx, cy, inner, start + slice, start, true);
991
+ ctx.closePath();
992
+ ctx.fillStyle = DONUT_COLORS[i2 % DONUT_COLORS.length];
993
+ ctx.fill();
994
+ start += slice;
995
+ }
996
+ var lightBg = isLightBackground();
997
+ var inkMain = lightBg ? "#1c2733" : "#e6ebf2";
998
+ var inkMuted = lightBg ? "rgba(28,39,51,.6)" : "rgba(225,231,240,.6)";
999
+ if (total <= 0) {
1000
+ ctx.strokeStyle = lightBg ? "rgba(128,128,128,.2)" : "rgba(255,255,255,.22)";
1001
+ ctx.lineWidth = 2;
1002
+ ctx.beginPath(); ctx.arc(cx, cy, (r + inner) / 2, 0, Math.PI * 2); ctx.stroke();
1003
+ }
1004
+ ctx.fillStyle = inkMain; ctx.font = "700 15px sans-serif"; ctx.textAlign = "center";
1005
+ ctx.fillText(total > 0 ? (mode === "calls" ? fmtInt(total) : fmtMoney(total)) : "—", cx, cy - 2);
1006
+ ctx.fillStyle = inkMuted; ctx.font = "10px sans-serif";
1007
+ ctx.fillText(mode === "calls" ? t("调用次数") : t("总消耗"), cx, cy + 13);
1008
+ }
1009
+ React.useEffect(function () {
1010
+ draw();
1011
+ function onResize() { draw(); }
1012
+ if (typeof window !== "undefined") window.addEventListener("resize", onResize);
1013
+ return function () { if (typeof window !== "undefined") window.removeEventListener("resize", onResize); };
1014
+ }, [rows, mode]);
1015
+ var tot = totalVal();
1016
+ return el("div", { style: { minWidth: 0 } },
1017
+ el("div", { style: Object.assign({}, st.calBar, { marginTop: 0, justifyContent: "center" }) },
1018
+ el("div", { style: st.seg },
1019
+ el("button", { style: mode === "cost" ? st.segBtnOn : st.segBtn, onClick: function () { setMode("cost"); } }, t("按消耗")),
1020
+ el("button", { style: mode === "calls" ? st.segBtnOn : st.segBtn, onClick: function () { setMode("calls"); } }, t("按调用数"))
1021
+ )
1022
+ ),
1023
+ el("div", { style: { display: "flex", justifyContent: "center" } },
1024
+ el("canvas", { ref: canvasRef, style: { width: 176, height: 176, display: "block" } })
1025
+ ),
1026
+ rows.length ? el("div", { style: st.legendRow },
1027
+ rows.slice(0, 10).map(function (r, idx) {
1028
+ var v = mode === "calls" ? (r.calls || 0) : r.cost;
1029
+ var p = tot > 0 ? Math.round(v / tot * 100) : 0;
1030
+ var nm = r.name || r.key || "—";
1031
+ return el("span", { key: "d-" + idx, title: nm, style: { display: "inline-flex", alignItems: "center", gap: 5, maxWidth: "100%" } },
1032
+ el("span", { style: Object.assign({}, st.legendDot, { background: DONUT_COLORS[idx % DONUT_COLORS.length] }) }),
1033
+ el("span", { style: { overflowWrap: "anywhere" } }, nm + " " + p + "%")
1034
+ );
1035
+ }),
1036
+ rows.length > 10 ? el("span", null, "…") : null
1037
+ ) : null
1038
+ );
1039
+ }
1040
+
1041
+ // ── 会话消耗排行 Top 10 ──
1042
+ function SessionRank(props) {
1043
+ var records = props.records || [], regime = props.regime || "auto";
1044
+ var bySid = {};
1045
+ for (var si = 0; si < records.length; si++) {
1046
+ var r = records[si];
1047
+ var sid = String(r.sessionId || "").trim();
1048
+ if (!sid) continue;
1049
+ var c = costOf(r, regime);
1050
+ var g = bySid[sid];
1051
+ if (!g) { g = bySid[sid] = { sid: sid, cost: 0, calls: 0, last: 0, tokens: 0 }; }
1052
+ g.cost += c; g.calls += 1; if (r.time > g.last) g.last = r.time;
1053
+ // 四桶互斥求和:不含 reasoningTokens —— 宿主 outputTokens 就是上游的
1054
+ // completion_tokens,reasoning 是它的子集,再加一遍等于凭空多算一笔。
1055
+ g.tokens += (r.inputTokens || 0) + (r.outputTokens || 0) + (r.cacheReadTokens || 0) + (r.cacheWriteTokens || 0);
1056
+ }
1057
+ var rows = [];
1058
+ for (var k in bySid) rows.push(bySid[k]);
1059
+ rows.sort(function (a, b) { return b.cost - a.cost || b.last - a.last; });
1060
+ rows = rows.slice(0, 10);
1061
+ if (rows.length === 0) return null;
1062
+ var maxCost = rows[0].cost;
1063
+ return el("div", { style: st.sessionCard },
1064
+ el("div", { style: st.chartPanelTitle }, t("会话消耗排行") + " · Top " + rows.length),
1065
+ rows.map(function (row, idx) {
1066
+ var pctv = maxCost > 0 ? Math.round(row.cost / maxCost * 100) : 0;
1067
+ var label = row.sid;
1068
+ if (label.length > 26) label = label.slice(0, 26) + "…";
1069
+ return el("div", { key: row.sid, style: st.sessionRow },
1070
+ el("span", { style: st.sessionName, title: row.sid }, (idx + 1) + ". " + label),
1071
+ el("div", { style: st.sessionBar },
1072
+ el("div", { style: Object.assign({}, st.sessionBarFill, { width: pctv + "%" }) })
1073
+ ),
1074
+ el("span", { style: st.sessionSub, title: t("总 token") + ": " + fmtInt(row.tokens) }, fmtInt(row.calls) + t(" 次 · ") + fmtInt(row.tokens) + " tok"),
1075
+ el("span", { style: st.sessionCost }, fmtMoney(row.cost))
1076
+ );
1077
+ })
1078
+ );
1079
+ }
1080
+
1081
+ // ── 共享聚合:概览页与明细页都按"同一次筛选结果"统计,避免各算一遍 ──
1082
+ function aggregateRecords(records, regime) {
1083
+ var totalCalls = records.length;
1084
+ var totalInterrupted = 0;
1085
+ var totalHit = 0, totalMiss = 0, totalWrite = 0, totalOut = 0, totalReason = 0, totalCost = 0, peakTotal = 0, offTotal = 0, peakCalls = 0, offCalls = 0;
1086
+ var byModel = {}, byProvider = {}, byProviderModel = {};
1087
+ for (var i = 0; i < records.length; i++) {
1088
+ var r = records[i];
1089
+ if (r.interrupted) totalInterrupted += 1;
1090
+ totalHit += r.cacheReadTokens || 0; totalMiss += r.inputTokens || 0; totalWrite += r.cacheWriteTokens || 0;
1091
+ totalOut += r.outputTokens || 0; totalReason += r.reasoningTokens || 0;
1092
+ var cost = costOf(r, regime);
1093
+ totalCost += cost;
1094
+ var gk = modelGroupKey(r);
1095
+ if (!byModel[gk]) byModel[gk] = { key: gk, name: modelName(r), calls: 0, hit: 0, miss: 0, out: 0, reason: 0, cost: 0, peakCost: 0, offCost: 0, peakCalls: 0, offCalls: 0 };
1096
+ byModel[gk].calls += 1; byModel[gk].hit += r.cacheReadTokens || 0; byModel[gk].miss += r.inputTokens || 0;
1097
+ byModel[gk].out += r.outputTokens || 0; byModel[gk].reason += r.reasoningTokens || 0; byModel[gk].cost += cost;
1098
+ var pk = providerGroupKey(r.provider);
1099
+ if (!byProvider[pk]) byProvider[pk] = { key: pk, name: providerName(r.provider), calls: 0, cost: 0, peakCost: 0, offCost: 0, peakCalls: 0, offCalls: 0, miss: 0, hit: 0, out: 0, reason: 0 };
1100
+ byProvider[pk].calls += 1; byProvider[pk].cost += cost;
1101
+ byProvider[pk].miss += r.inputTokens || 0; byProvider[pk].hit += r.cacheReadTokens || 0;
1102
+ byProvider[pk].out += r.outputTokens || 0; byProvider[pk].reason += r.reasoningTokens || 0;
1103
+ var pmKey = pk + "||" + gk;
1104
+ if (!byProviderModel[pmKey]) byProviderModel[pmKey] = { key: pmKey, providerKey: pk, providerName: providerName(r.provider), modelKey: gk, modelName: modelName(r), calls: 0, cost: 0, peakCost: 0, offCost: 0, peakCalls: 0, offCalls: 0, miss: 0, hit: 0, out: 0, reason: 0 };
1105
+ byProviderModel[pmKey].calls += 1; byProviderModel[pmKey].cost += cost;
1106
+ byProviderModel[pmKey].miss += r.inputTokens || 0; byProviderModel[pmKey].hit += r.cacheReadTokens || 0;
1107
+ byProviderModel[pmKey].out += r.outputTokens || 0; byProviderModel[pmKey].reason += r.reasoningTokens || 0;
1108
+ if (r.peak) {
1109
+ byModel[gk].peakCost += cost; byModel[gk].peakCalls += 1;
1110
+ byProvider[pk].peakCost += cost; byProvider[pk].peakCalls += 1;
1111
+ byProviderModel[pmKey].peakCost += cost; byProviderModel[pmKey].peakCalls += 1;
1112
+ peakTotal += cost; peakCalls += 1;
1113
+ } else {
1114
+ byModel[gk].offCost += cost; byModel[gk].offCalls += 1;
1115
+ byProvider[pk].offCost += cost; byProvider[pk].offCalls += 1;
1116
+ byProviderModel[pmKey].offCost += cost; byProviderModel[pmKey].offCalls += 1;
1117
+ offTotal += cost; offCalls += 1;
1118
+ }
1119
+ }
1120
+ var hitRate = (totalHit + totalMiss + totalWrite) > 0 ? (totalHit / (totalHit + totalMiss + totalWrite) * 100).toFixed(1) + "%" : "—";
1121
+ var modelRows = [];
1122
+ for (var k in byModel) modelRows.push(byModel[k]);
1123
+ modelRows.sort(function (a, b) { return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; });
1124
+ // 环形图:按消耗降序
1125
+ var donutRows = [];
1126
+ 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 });
1127
+ donutRows.sort(function (a, b) { return b.cost - a.cost || (a.key < b.key ? -1 : 1); });
1128
+ for (var mr = 0; mr < modelRows.length; mr++) {
1129
+ var pset = {};
1130
+ for (var rr = 0; rr < records.length; rr++) {
1131
+ if (modelGroupKey(records[rr]) === modelRows[mr].key) pset[providerName(records[rr].provider)] = true;
1132
+ }
1133
+ modelRows[mr].providerText = Object.keys(pset).sort().join(" / ") || t("未知服务商");
1134
+ }
1135
+ var providerRows = [];
1136
+ for (var kp in byProvider) providerRows.push(byProvider[kp]);
1137
+ providerRows.sort(function (a, b) { return b.cost - a.cost || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0); });
1138
+ // 服务商 × 模型分组:每个服务商一组,组内按消耗排序
1139
+ var providerGroups = [];
1140
+ for (var pg = 0; pg < providerRows.length; pg++) {
1141
+ var gprov = providerRows[pg];
1142
+ var models = [];
1143
+ for (var km in byProviderModel) {
1144
+ if (byProviderModel[km].providerKey === gprov.key) models.push(byProviderModel[km]);
1145
+ }
1146
+ models.sort(function (a, b) { return b.cost - a.cost || (a.modelKey < b.modelKey ? -1 : 1); });
1147
+ providerGroups.push({ provider: gprov, models: models });
1148
+ }
1149
+ return {
1150
+ totalCalls: totalCalls, totalInterrupted: totalInterrupted,
1151
+ totalHit: totalHit, totalMiss: totalMiss, totalWrite: totalWrite, totalOut: totalOut,
1152
+ totalReason: totalReason, totalCost: totalCost,
1153
+ peakTotal: peakTotal, offTotal: offTotal, peakCalls: peakCalls, offCalls: offCalls,
1154
+ hitRate: hitRate, modelRows: modelRows, donutRows: donutRows,
1155
+ providerRows: providerRows, providerGroups: providerGroups
1156
+ };
1157
+ }
1158
+
1159
+ // ── 明细视图(按模型 / 按服务商)──
1160
+ // 表格原本长在概览页里,与"今天花了多少"混在一起;现在独立成"明细"页签。
1161
+ function BreakdownView(props) {
1162
+ var records = props.records || [], regime = props.regime;
1163
+ var modeState = React.useState("model");
1164
+ var mode = modeState[0], setMode = modeState[1];
1165
+ // 切换「按模型 / 按服务商」时不必重新聚合整份记录
1166
+ var agg = React.useMemo(function () {
1167
+ return aggregateRecords(records, regime);
1168
+ }, [props.records, regime]);
1169
+ var modelRows = agg.modelRows, providerRows = agg.providerRows;
1170
+ var totalCalls = agg.totalCalls, totalMiss = agg.totalMiss, totalHit = agg.totalHit;
1171
+ var totalOut = agg.totalOut, totalReason = agg.totalReason, totalCost = agg.totalCost;
1172
+ var peakTotal = agg.peakTotal, offTotal = agg.offTotal, peakCalls = agg.peakCalls, offCalls = agg.offCalls;
1173
+
1174
+ var pmRows = [];
1175
+ for (var gi = 0; gi < agg.providerGroups.length; gi++) {
1176
+ var g = agg.providerGroups[gi];
1177
+ var gprov2 = g.provider;
1178
+ pmRows.push(el("tr", { key: "g-" + gprov2.key },
1179
+ el("td", { style: st.tdGroup }, gprov2.name),
1180
+ el("td", { style: st.tdGroup }, "(" + g.models.length + t(" 个模型)")),
1181
+ el("td", { style: st.tdGroupR }, fmtInt(gprov2.calls)),
1182
+ el("td", { style: st.tdGroupR }, el("span", { style: st.badgePeak }, fmtInt(gprov2.peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(gprov2.offCalls))),
1183
+ el("td", { style: st.tdGroupR }, fmtInt(gprov2.miss)),
1184
+ el("td", { style: st.tdGroupR }, fmtInt(gprov2.hit)),
1185
+ el("td", { style: st.tdGroupR }, fmtInt(gprov2.out)),
1186
+ el("td", { style: st.tdGroupR }, gprov2.reason ? fmtInt(gprov2.reason) : "—"),
1187
+ el("td", { style: st.tdGroupR }, fmtMoney(gprov2.peakCost)),
1188
+ el("td", { style: st.tdGroupR }, fmtMoney(gprov2.offCost)),
1189
+ el("td", { style: st.tdGroupR }, fmtMoney(gprov2.cost))
1190
+ ));
1191
+ for (var mi = 0; mi < g.models.length; mi++) {
1192
+ var md = g.models[mi];
1193
+ pmRows.push(el("tr", { key: "pm-" + md.key },
1194
+ el("td", { style: st.tdFirst }, ""),
1195
+ el("td", { style: st.tdWrap }, md.modelName),
1196
+ el("td", { style: st.td }, fmtInt(md.calls)),
1197
+ el("td", { style: st.td }, el("span", { style: st.badgePeak }, fmtInt(md.peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(md.offCalls))),
1198
+ el("td", { style: st.td }, fmtInt(md.miss)),
1199
+ el("td", { style: st.td }, el("span", { style: st.badgeHit }, fmtInt(md.hit))),
1200
+ el("td", { style: st.td }, fmtInt(md.out)),
1201
+ el("td", { style: st.td }, md.reason ? fmtInt(md.reason) : "—"),
1202
+ el("td", { style: st.td }, fmtMoney(md.peakCost)),
1203
+ el("td", { style: st.td }, fmtMoney(md.offCost)),
1204
+ el("td", { style: st.td }, fmtMoney(md.cost))
1205
+ ));
1206
+ }
1207
+ }
1208
+ pmRows.push(el("tr", { key: "g-total" },
1209
+ el("td", { style: st.tdTotalFirst }, t("总费用合计")),
1210
+ el("td", { style: st.tdTotal }, fmtInt(providerRows.length) + t(" 个服务商")),
1211
+ el("td", { style: st.tdTotal }, fmtInt(totalCalls)),
1212
+ el("td", { style: st.tdTotal }, el("span", { style: st.badgePeak }, fmtInt(peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(offCalls))),
1213
+ el("td", { style: st.tdTotal }, fmtInt(totalMiss)),
1214
+ el("td", { style: st.tdTotal }, fmtInt(totalHit)),
1215
+ el("td", { style: st.tdTotal }, fmtInt(totalOut)),
1216
+ el("td", { style: st.tdTotal }, totalReason ? fmtInt(totalReason) : "—"),
1217
+ el("td", { style: st.tdTotal }, fmtMoney(peakTotal)),
1218
+ el("td", { style: st.tdTotal }, fmtMoney(offTotal)),
1219
+ el("td", { style: st.tdTotal }, fmtMoney(totalCost))
1220
+ ));
1221
+
1222
+ return el("div", null,
1223
+ el("div", { style: st.calBar },
1224
+ el("div", { style: st.seg },
1225
+ el("button", { style: mode === "model" ? st.segBtnOn : st.segBtn, onClick: function () { setMode("model"); } }, t("按模型")),
1226
+ el("button", { style: mode === "provider" ? st.segBtnOn : st.segBtn, onClick: function () { setMode("provider"); } }, t("按服务商"))
1227
+ ),
1228
+ el("span", { style: st.note }, fmtInt(modelRows.length) + t(" 个模型") + " · " + fmtInt(providerRows.length) + t(" 个服务商") + " · " + fmtInt(totalCalls) + t(" 条记录"))
1229
+ ),
1230
+ mode === "model"
1231
+ ? el("div", { style: st.scroll },
1232
+ el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
1233
+ el("thead", null, el("tr", null,
1234
+ 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("高峰/空闲")),
1235
+ el("th", { style: st.th }, t("输入·未命中")), el("th", { style: st.th }, t("缓存命中")), el("th", { style: st.th }, t("输出")), el("th", { style: st.th }, t("推理")),
1236
+ el("th", { style: st.th }, t("高峰消耗")), el("th", { style: st.th }, t("空闲消耗")), el("th", { style: st.th }, t("总消耗"))
1237
+ )),
1238
+ el("tbody", null,
1239
+ modelRows.map(function (m) {
1240
+ return el("tr", { key: m.key },
1241
+ el("td", { style: st.tdModelName, title: m.name }, m.name),
1242
+ el("td", { style: st.tdModelProvider, title: m.providerText }, m.providerText),
1243
+ el("td", { style: st.td }, fmtInt(m.calls)),
1244
+ el("td", { style: st.td }, el("span", { style: st.badgePeak }, fmtInt(m.peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(m.offCalls))),
1245
+ el("td", { style: st.td }, fmtInt(m.miss)),
1246
+ el("td", { style: st.td }, el("span", { style: st.badgeHit }, fmtInt(m.hit))),
1247
+ el("td", { style: st.td }, fmtInt(m.out)),
1248
+ el("td", { style: st.td }, m.reason ? fmtInt(m.reason) : "—"),
1249
+ el("td", { style: st.td }, fmtMoney(m.peakCost)),
1250
+ el("td", { style: st.td }, fmtMoney(m.offCost)),
1251
+ el("td", { style: Object.assign({}, st.td, { fontWeight: 600, color: C.accentText }) }, fmtMoney(m.cost))
1252
+ );
1253
+ }),
1254
+ el("tr", null,
1255
+ el("td", { style: st.tdTotalFirst }, t("合计")),
1256
+ el("td", { style: st.tdTotalFirst }, fmtInt(providerRows.length) + t(" 个服务商")),
1257
+ el("td", { style: st.tdTotal }, fmtInt(totalCalls)),
1258
+ el("td", { style: st.tdTotal }, el("span", { style: st.badgePeak }, fmtInt(peakCalls)), " / ", el("span", { style: st.badgeValley }, fmtInt(offCalls))),
1259
+ el("td", { style: st.tdTotal }, fmtInt(totalMiss)),
1260
+ el("td", { style: st.tdTotal }, fmtInt(totalHit)),
1261
+ el("td", { style: st.tdTotal }, fmtInt(totalOut)),
1262
+ el("td", { style: st.tdTotal }, totalReason ? fmtInt(totalReason) : "—"),
1263
+ el("td", { style: st.tdTotal }, fmtMoney(peakTotal)),
1264
+ el("td", { style: st.tdTotal }, fmtMoney(offTotal)),
1265
+ el("td", { style: st.tdTotal }, fmtMoney(totalCost))
1266
+ )
1267
+ )
1268
+ )
1269
+ )
1270
+ : el("div", { style: st.scroll },
1271
+ el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
1272
+ el("thead", null, el("tr", null,
1273
+ el("th", { style: st.thFirst }, t("API 服务商")), el("th", { style: st.thFirst }, t("模型")), el("th", { style: st.th }, t("调用")), el("th", { style: st.th }, t("高峰/空闲")),
1274
+ el("th", { style: st.th }, t("输入·未命中")), el("th", { style: st.th }, t("缓存命中")), el("th", { style: st.th }, t("输出")), el("th", { style: st.th }, t("推理")),
1275
+ el("th", { style: st.th }, t("高峰消耗")), el("th", { style: st.th }, t("空闲消耗")), el("th", { style: st.th }, t("总消耗"))
1276
+ )),
1277
+ el("tbody", null, pmRows)
1278
+ )
1279
+ )
1280
+ );
1281
+ }
1282
+
1283
+ // ── Overview view ──
1284
+ function OverviewView(props) {
1285
+ var allRecords = props.records, regime = props.regime;
1286
+ var days = props.days || [], budget = props.budget || null;
1287
+ var toolCalls = props.toolCalls || [];
1288
+ var toolOpenState = React.useState(false);
1289
+ var toolOpen = toolOpenState[0], setToolOpen = toolOpenState[1];
1290
+ var presetState = React.useState("all");
1291
+ var preset = presetState[0], setPreset = presetState[1];
1292
+ var fromState = React.useState("");
1293
+ var fromDate = fromState[0], setFromDate = fromState[1];
1294
+ var toState = React.useState("");
1295
+ var toDate = toState[0], setToDate = toState[1];
1296
+ // 口径提示默认收起:只有存在估算记录或旧记录时才出现
1297
+ var scopeState = React.useState(false);
1298
+ var scopeOpen = scopeState[0], setScopeOpen = scopeState[1];
1299
+
1300
+ var nowPeriod = periodNow(Date.now());
1301
+ // 本月(北京时间自然月)已消耗 / 调用 / 日均 / 峰值日:不受概览日期筛选影响
1302
+ var monthCost = 0, monthCalls = 0, monthDayMap = {};
1303
+ var nowB = new Date(Date.now() + 8 * 3600 * 1000);
1304
+ var ymPrefix = nowB.getUTCFullYear() + "-" + pad2(nowB.getUTCMonth() + 1);
1305
+ var monthStartMs = Date.UTC(nowB.getUTCFullYear(), nowB.getUTCMonth(), 1) - 8 * 3600 * 1000;
1306
+ var prevMonthStartMs = Date.UTC(nowB.getUTCFullYear(), nowB.getUTCMonth() - 1, 1) - 8 * 3600 * 1000;
1307
+ var prevMonthCost = 0;
1308
+ for (var mi = 0; mi < allRecords.length; mi++) {
1309
+ var mr = allRecords[mi];
1310
+ var mk2 = bjKey(mr.time);
1311
+ if (mk2.indexOf(ymPrefix) === 0) {
1312
+ var mc = costOf(mr, "auto");
1313
+ monthCost += mc;
1314
+ monthCalls += 1;
1315
+ if (!monthDayMap[mk2]) monthDayMap[mk2] = 0;
1316
+ monthDayMap[mk2] += mc;
1317
+ } else if (mr.time >= prevMonthStartMs && mr.time < monthStartMs) {
1318
+ prevMonthCost += costOf(mr, "auto");
1319
+ }
1320
+ }
1321
+ var moDelta = prevMonthCost > 0 ? (monthCost - prevMonthCost) / prevMonthCost * 100 : null;
1322
+ var peakDayKey = "", peakDayCost = 0;
1323
+ for (var pdk in monthDayMap) { if (monthDayMap[pdk] > peakDayCost) { peakDayCost = monthDayMap[pdk]; peakDayKey = pdk; } }
1324
+ var monthAvgCost = nowB.getUTCDate() > 0 ? monthCost / nowB.getUTCDate() : 0;
1325
+ var budgetPct = budget && budget.monthly > 0 ? Math.min(100, monthCost / budget.monthly * 100) : 0;
1326
+ function budgetStyle() {
1327
+ if (budgetPct >= 100) return st.budgetHeroFillDanger;
1328
+ if (budgetPct >= 80) return st.budgetHeroFillWarn;
1329
+ return st.budgetHeroFill;
1330
+ }
1331
+ function budgetText() {
1332
+ if (budgetPct >= 100) return t("超支 ") + fmtMoney(monthCost - budget.monthly);
1333
+ return t("已用 ") + budgetPct.toFixed(0) + "% · " + t("剩余 ") + fmtMoney(budget.monthly - monthCost);
1334
+ }
1335
+
1336
+ var nowKey = bjKey(Date.now());
1337
+ var fromMs = null, toMs = null;
1338
+ if (preset === "today") { fromMs = bjStartMs(nowKey); toMs = Date.now(); }
1339
+ else if (preset === "7d") { fromMs = bjStartMs(nowKey) - 6 * 86400000; toMs = Date.now(); }
1340
+ else if (preset === "30d") { fromMs = bjStartMs(nowKey) - 29 * 86400000; toMs = Date.now(); }
1341
+ else if (preset === "custom") {
1342
+ if (fromDate) fromMs = bjStartMs(fromDate);
1343
+ if (toDate) toMs = bjStartMs(toDate) + 86400000 - 1;
1344
+ }
1345
+ var records = [];
1346
+ for (var fi = 0; fi < allRecords.length; fi++) {
1347
+ var fr = allRecords[fi];
1348
+ if (fromMs != null && fr.time < fromMs) continue;
1349
+ if (toMs != null && fr.time > toMs) continue;
1350
+ records.push(fr);
1351
+ }
1352
+ // 聚合统一走 aggregateRecords():概览页与明细页用的是同一套口径,
1353
+ // 不再各自复制一遍这个分组循环。用 useMemo 绑定"真正影响结果"的输入,
1354
+ // 展开抽屉、切换显示模式等本页 state 变化时不再重算上万条记录。
1355
+ var agg = React.useMemo(function () {
1356
+ return aggregateRecords(records, regime);
1357
+ }, [allRecords, preset, fromDate, toDate, regime]);
1358
+ var totalCalls = agg.totalCalls, totalInterrupted = agg.totalInterrupted;
1359
+ var totalHit = agg.totalHit, totalMiss = agg.totalMiss, totalWrite = agg.totalWrite;
1360
+ var totalOut = agg.totalOut, totalReason = agg.totalReason, totalCost = agg.totalCost;
1361
+ var peakTotal = agg.peakTotal, offTotal = agg.offTotal, peakCalls = agg.peakCalls, offCalls = agg.offCalls;
1362
+ var hitRate = agg.hitRate, modelRows = agg.modelRows, donutRows = agg.donutRows;
1363
+ var providerRows = agg.providerRows;
1364
+ // 估算记录(DeepSeek 搜索后端):token 与费用都只是下限估算,不是上游实测值。
1365
+ // 必须与实测记录分开点名说明,否则用户会把估算当成账单。
1366
+ var estCalls = 0, estCost = 0;
1367
+ for (var esti = 0; esti < records.length; esti++) {
1368
+ var estr = records[esti];
1369
+ if (!estr.estimated) continue;
1370
+ estCalls += 1;
1371
+ estCost += costOf(estr, regime);
1372
+ }
1373
+ // 内部工具/占位调用(单独一个可折叠分组)
1374
+ var toolAgg = {};
1375
+ for (var ti = 0; ti < toolCalls.length; ti++) {
1376
+ var tr = toolCalls[ti];
1377
+ var tk = (tr.provider || '') + '|' + (tr.model || '');
1378
+ var tg = toolAgg[tk];
1379
+ if (!tg) { tg = toolAgg[tk] = { provider: tr.provider || '—', model: tr.model || '—', calls: 0, cost: 0 }; }
1380
+ tg.calls += 1; tg.cost += tr.autoCost || 0;
1381
+ }
1382
+ var toolRows = [];
1383
+ for (var tki in toolAgg) toolRows.push(toolAgg[tki]);
1384
+ toolRows.sort(function (a, b) { return b.calls - a.calls || (a.provider < b.provider ? -1 : 1); });
1385
+ function presetBtn(k, label) {
1386
+ return el("button", { style: preset === k ? st.segBtnOn : st.segBtn, onClick: function () { setPreset(k); } }, label);
1387
+ }
1388
+ function setCustomFrom(v) { setFromDate(v); setPreset("custom"); }
1389
+ function setCustomTo(v) { setToDate(v); setPreset("custom"); }
1390
+
1391
+ return el("div", null,
1392
+ el("div", { style: st.calBar },
1393
+ el("div", { style: st.seg },
1394
+ presetBtn("today", t("今天")), presetBtn("7d", t("近7天")), presetBtn("30d", t("近30天")), presetBtn("all", t("全部"))
1395
+ ),
1396
+ el("span", { style: st.note }, t("自定义区间:")),
1397
+ el("input", { type: "date", style: st.dateInput, value: fromDate, onChange: function (e) { setCustomFrom(e.target.value); } }),
1398
+ el("span", { style: st.note }, t("至")),
1399
+ el("input", { type: "date", style: st.dateInput, value: toDate, onChange: function (e) { setCustomTo(e.target.value); } }),
1400
+ (fromDate || toDate || preset !== "all") ? el("button", { style: st.btn, onClick: function () { setFromDate(""); setToDate(""); setPreset("all"); } }, t("清除筛选")) : null
1401
+ ),
1402
+ el("div", { style: st.note, marginTop: 8 },
1403
+ t("当前范围:共 ") + fmtInt(records.length) + t(" 条") + (preset === "all" && !fromDate && !toDate ? t("(全部记录)") : "") + t(" · 总消耗 ") + fmtMoney(totalCost) + "(" + costHint(regime) + ")"
1404
+ ),
1405
+ // 一行摘要:本月已消耗 + 预算进度 + 当前时段。
1406
+ // 原先这里是一块高饱和蓝色 hero 卡(月初几乎恒显示 ¥0.0000)+ 独立预算卡,
1407
+ // 视觉权重过高却信息量很低。
1408
+ el("div", { style: st.summaryStrip },
1409
+ el("div", { style: st.summaryMain },
1410
+ el("span", { style: st.summaryLabel }, t("本月已消耗")),
1411
+ el("span", { style: st.summaryValue }, fmtMoney(monthCost)),
1412
+ monthCalls ? el("span", { style: st.summarySub }, fmtInt(monthCalls) + t(" 次")) : null,
1413
+ moDelta != null ? el("span", { style: moDelta > 0 ? st.badgeUp : st.badgeDown }, (moDelta > 0 ? "▲ " : "▼ ") + Math.abs(moDelta).toFixed(1) + "% " + t("较上月")) : null
1414
+ ),
1415
+ budget && budget.monthly > 0
1416
+ ? el("div", { style: st.summaryBudget },
1417
+ el("div", { style: Object.assign({}, st.budgetHeroBar, { flex: 1 }) },
1418
+ el("div", { style: Object.assign({}, budgetStyle(), { width: Math.max(budgetPct > 0 ? 4 : 0, budgetPct) + "%" }) })
1419
+ ),
1420
+ el("span", { style: st.summarySub }, budgetText())
1421
+ )
1422
+ : null,
1423
+ el("span", { style: nowPeriod.peak ? st.badgePeak : st.badgeValley }, t("当前 · ") + t(nowPeriod.label))
1424
+ ),
1425
+ // 关键指标:只保留 4 张卡。原先这里是「3 张大卡 + 4 张卡 + 5 格 token 条」,
1426
+ // 同一批数字铺了三层;高峰/空闲、未命中/输出/推理都收进对应的 hint。
1427
+ el("div", { style: st.cards, marginTop: 10 },
1428
+ el(Card, {
1429
+ primary: true,
1430
+ label: t("总消耗"),
1431
+ value: fmtMoney(totalCost),
1432
+ hint: t("高峰 ") + fmtMoney(peakTotal) + t(" · 空闲 ") + fmtMoney(offTotal)
1433
+ }),
1434
+ el(Card, {
1435
+ label: t("调用次数"),
1436
+ value: fmtInt(totalCalls),
1437
+ hint: t("高峰 ") + fmtInt(peakCalls) + t(" · 空闲 ") + fmtInt(offCalls) + (totalInterrupted > 0 ? t(" · 中断 ") + fmtInt(totalInterrupted) + t("(未计费)") : "")
1438
+ }),
1439
+ el(Card, { label: t("缓存命中率"), value: hitRate, hint: t("输入 · 缓存命中 ") + fmtInt(totalHit) }),
1440
+ el(Card, {
1441
+ label: t("总 token"),
1442
+ value: fmtInt(totalMiss + totalHit + totalWrite + totalOut),
1443
+ hint: t("未命中 ") + fmtInt(totalMiss) + t(" · 输出 ") + fmtInt(totalOut) + (totalReason ? t(" · 推理 ") + fmtInt(totalReason) : "")
1444
+ })
1445
+ ),
1446
+ // 口径提示:只有"确实需要说明"时才出现(估算记录 / 旧记录缺冻结单价),
1447
+ // 两条合并进同一个抽屉、默认收起,不再各占一块橙色告警框。
1448
+ (estCalls > 0 || props.legacyPricingCount > 0)
1449
+ ? el("div", { style: st.scopeBar },
1450
+ el("button", { style: scopeOpen ? st.btnPrimary : st.btn, onClick: function () { setScopeOpen(!scopeOpen); } },
1451
+ t("统计口径提示")
1452
+ + (estCalls > 0 ? " · " + fmtInt(estCalls) + t(" 次估算") : "")
1453
+ + (props.legacyPricingCount > 0 ? " · " + fmtInt(props.legacyPricingCount) + t(" 条无冻结单价") : "")),
1454
+ el("span", { style: st.note }, t("金额以 DeepSeek 官方后台账单为准"))
1455
+ )
1456
+ : null,
1457
+ (estCalls > 0 || props.legacyPricingCount > 0) && scopeOpen
1458
+ ? el("div", { style: Object.assign({}, st.diffbox, { marginTop: 8 }) },
1459
+ estCalls > 0
1460
+ ? el("div", null,
1461
+ el("div", { style: st.diffboxTitle }, t("统计口径:web_search 搜索后端未纳入精确统计")),
1462
+ el("div", { style: st.diffboxText },
1463
+ t("DeepSeek 搜索后端调用由 Harness 用原生请求直连官方接口,响应用量未持久化到会话日志,插件无法取到实际 token。此处只记录次数,并按请求体给出输入侧下限估算:共 ")
1464
+ + fmtInt(estCalls) + t(" 次,估算消耗 ≥ ") + fmtMoney(estCost)
1465
+ + t("。搜索在服务端注入检索结果并可能多轮生成,输入与输出都会高于此值,官方账单必然更高。")),
1466
+ el("div", { style: st.diffboxWarn }, t("该部分为估算下限,请以 DeepSeek 官方后台账单为准。")))
1467
+ : null,
1468
+ props.legacyPricingCount > 0
1469
+ ? el("div", { style: { marginTop: estCalls > 0 ? 8 : 0 } },
1470
+ el("div", { style: st.diffboxTitle }, t("历史价格提示")),
1471
+ el("div", { style: st.diffboxText },
1472
+ t("有 ") + fmtInt(props.legacyPricingCount) + t(" 条记录没有冻结单价(旧版本写入或手工导入),它们的费用按当前价格表计算,改动价格表后会跟着变化;新记录已冻结写入时生效的单价,不受后续改价影响。")))
1473
+ : null,
1474
+ el("div", { style: st.diffboxText, marginTop: 8 },
1475
+ t("计价说明:DeepSeek 官方请求按官方峰谷价;SiliconFlow 按自身人民币公开价;DigitalOcean 按美元公开价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。"))
1476
+ )
1477
+ : null,
1478
+ el("div", { style: st.chartGrid },
1479
+ el("div", { style: st.chartPanel },
1480
+ el("div", { style: st.chartPanelTitle }, t("消耗趋势(近 30 天)")),
1481
+ el(TrendChart, { days: days, regime: regime })
1482
+ ),
1483
+ el("div", { style: st.chartPanel },
1484
+ el("div", { style: st.chartPanelTitle }, t("模型消耗占比")),
1485
+ el(DonutChart, { rows: donutRows })
1486
+ )
1487
+ ),
1488
+ // 明细表格已移到「明细」页签(BreakdownView)——概览页只留"看趋势 + 看结论"。
1489
+ el(SessionRank, { records: records, regime: regime }),
1490
+ toolCalls.length ? el("div", { style: st.sec, marginTop: 14 },
1491
+ el("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
1492
+ el("span", { style: { fontSize: fs(FS.lg), fontWeight: 600 } }, t("工具调用(内部)") + " · " + fmtInt(toolCalls.length) + t(" 条内部调用")),
1493
+ el("button", { style: st.btn, onClick: function () { setToolOpen(!toolOpen); } }, toolOpen ? t("收起") : t("展开"))
1494
+ ),
1495
+ toolOpen ? el("div", { style: st.scroll },
1496
+ el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
1497
+ el("thead", null, el("tr", null,
1498
+ el("th", { style: st.thFirst }, t("API 服务商 / 模型")),
1499
+ el("th", { style: st.th }, t("调用")),
1500
+ el("th", { style: st.th }, t("总消耗"))
1501
+ )),
1502
+ el("tbody", null, toolRows.map(function (tg, idx) {
1503
+ return el("tr", { key: String(idx) },
1504
+ el("td", { style: st.tdFirst }, tg.model + " · " + tg.provider),
1505
+ el("td", { style: st.td }, fmtInt(tg.calls)),
1506
+ el("td", { style: st.td }, fmtMoney(tg.cost))
1507
+ );
1508
+ }))
1509
+ )
1510
+ ) : null
1511
+ ) : null
1512
+ );
1513
+ }
1514
+
1515
+ // ── Calendar view ──
1516
+ function CalendarView(props) {
1517
+ var records = props.records, regime = props.regime, days = props.days || [];
1518
+ var nowB = new Date(Date.now() + 8 * 3600 * 1000);
1519
+ var nowY = nowB.getUTCFullYear(), nowM = nowB.getUTCMonth();
1520
+ var ymState = React.useState({ y: nowY, m: nowM });
1521
+ var ym = ymState[0], setYm = ymState[1];
1522
+ var selState = React.useState(null);
1523
+ var selectedDay = selState[0], setSelectedDay = selState[1];
1524
+ var dimState = React.useState("cost");
1525
+ var dimMode = dimState[0], setDim = dimState[1];
1526
+ var presetState = React.useState("all");
1527
+ var preset = presetState[0], setPreset = presetState[1];
1528
+ var fromState = React.useState("");
1529
+ var fromDate = fromState[0], setFromDate = fromState[1];
1530
+ var toState = React.useState("");
1531
+ var toDate = toState[0], setToDate = toState[1];
1532
+
1533
+ // 日期范围筛选:预设(今天/近7天/近30天/全部)+ 自定义起止,影响每日统计表与顶部卡片。
1534
+ var nowKey = bjKey(Date.now());
1535
+ var fromMs = null, toMs = null;
1536
+ if (preset === "today") { fromMs = bjStartMs(nowKey); toMs = Date.now(); }
1537
+ else if (preset === "7d") { fromMs = bjStartMs(nowKey) - 6 * 86400000; toMs = Date.now(); }
1538
+ else if (preset === "30d") { fromMs = bjStartMs(nowKey) - 29 * 86400000; toMs = Date.now(); }
1539
+ else if (preset === "custom") {
1540
+ if (fromDate) fromMs = bjStartMs(fromDate);
1541
+ if (toDate) toMs = bjStartMs(toDate) + 86400000 - 1;
1542
+ }
1543
+ var rangeActive = preset !== "all" || fromDate || toDate;
1544
+ function inRangeKey(key) {
1545
+ if (!rangeActive) return true;
1546
+ var ms = bjStartMs(key);
1547
+ if (fromMs != null && ms < fromMs) return false;
1548
+ if (toMs != null && ms > toMs) return false;
1549
+ return true;
1550
+ }
1551
+
1552
+ var y = ym.y, m = ym.m;
1553
+ var nDays = monthDays(y, m);
1554
+ var offset = monthOffset(y, m);
1555
+ var dayMap = {};
1556
+ for (var di = 0; di < days.length; di++) dayMap[days[di].day] = days[di];
1557
+
1558
+ // 高峰/空闲消耗拆分:优先取 host 汇总的 days 字段,字段缺失时用记录回退计算
1559
+ var splitMap = {};
1560
+ for (var sr = 0; sr < records.length; sr++) {
1561
+ var rec0 = records[sr];
1562
+ var k0 = bjKey(rec0.time);
1563
+ var c0 = costOf(rec0, regime);
1564
+ if (!splitMap[k0]) splitMap[k0] = { peak: 0, off: 0 };
1565
+ if (rec0.peak) splitMap[k0].peak += c0; else splitMap[k0].off += c0;
1566
+ }
1567
+
1568
+ function dayCost(d) { return costOf(d, regime); }
1569
+ function dayVal(d) { return dimMode === "cost" ? dayCost(d) : (d ? d.calls : 0); }
1570
+
1571
+ var maxV = 0, monthCalls = 0, monthMiss = 0, monthHit = 0, monthOut = 0, monthCost = 0, monthPeak = 0, monthOff = 0, monthPeakCalls = 0, monthOffCalls = 0;
1572
+ for (var d2 = 1; d2 <= nDays; d2++) {
1573
+ var key2 = y + "-" + pad2(m + 1) + "-" + pad2(d2);
1574
+ var r2 = dayMap[key2];
1575
+ var inR2 = inRangeKey(key2);
1576
+ var v2 = r2 && inR2 ? dayVal(r2) : 0;
1577
+ if (v2 > maxV) maxV = v2;
1578
+ if (r2 && inR2) {
1579
+ monthCalls += r2.calls; monthMiss += r2.miss; monthHit += r2.hit; monthOut += r2.out;
1580
+ monthPeakCalls += r2.peakCalls || 0; monthOffCalls += r2.offPeakCalls || 0;
1581
+ var ds2 = daySplit(r2, regime, splitMap[key2]);
1582
+ monthPeak += ds2.peak; monthOff += ds2.off;
1583
+ }
1584
+ }
1585
+ monthCost = monthPeak + monthOff;
1586
+
1587
+ var weeks = weekdayLabels();
1588
+ var cells = [];
1589
+ for (var b = 0; b < offset; b++) cells.push(el("div", { key: "b" + b, style: st.calCellBlank }, ""));
1590
+ for (var d3 = 1; d3 <= nDays; d3++) {
1591
+ var key3 = y + "-" + pad2(m + 1) + "-" + pad2(d3);
1592
+ var r3 = dayMap[key3];
1593
+ var inR3 = inRangeKey(key3);
1594
+ var v3 = r3 && inR3 ? dayVal(r3) : 0;
1595
+ var inten = maxV > 0 ? v3 / maxV : 0;
1596
+ var bg = r3 && inR3 && v3 > 0 ? "rgba(46,134,222," + (0.12 + 0.66 * inten).toFixed(2) + ")" : "rgba(128,128,128,.06)";
1597
+ var fg = inten > 0.5 ? "#fff" : "inherit";
1598
+ var tip = r3 && inR3
1599
+ ? (function () {
1600
+ var ds3 = daySplit(r3, regime, splitMap[key3]);
1601
+ 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) + ")";
1602
+ })()
1603
+ : dayLabel(key3) + t("\n无记录");
1604
+ var isSel = selectedDay === key3;
1605
+ cells.push(el("div", {
1606
+ key: key3,
1607
+ title: tip,
1608
+ onClick: (function (k) { return function () { setSelectedDay(selectedDay === k ? null : k); }; })(key3),
1609
+ style: Object.assign({ background: bg, color: fg }, st.calCell, isSel ? st.calCellOn : null)
1610
+ },
1611
+ el("div", null, String(d3)),
1612
+ r3 && inR3 ? el("div", { style: st.calCellVal }, dimMode === "cost" ? fmtMoney(dayCost(r3)) : fmtInt(r3.calls)) : el("div", { style: st.calCellVal }, "—")
1613
+ ));
1614
+ }
1615
+
1616
+ // selected day detail (newest first)
1617
+ var selRecords = [];
1618
+ var selPeakCalls = 0, selOffCalls = 0, selPeakCost = 0, selOffCost = 0;
1619
+ if (selectedDay) {
1620
+ for (var si = 0; si < records.length; si++) {
1621
+ if (bjKey(records[si].time) === selectedDay) selRecords.push(records[si]);
1622
+ }
1623
+ selRecords.sort(function (a, b) { return b.time - a.time; });
1624
+ for (var sj = 0; sj < selRecords.length; sj++) {
1625
+ var srec = selRecords[sj];
1626
+ var scost = costOf(srec, regime);
1627
+ if (srec.peak) { selPeakCalls++; selPeakCost += scost; }
1628
+ else { selOffCalls++; selOffCost += scost; }
1629
+ }
1630
+ }
1631
+
1632
+ // month daily stats (desc). 范围筛选激活时:跨月取范围内所有天数;否则取当前月。
1633
+ var monthRows = [];
1634
+ if (rangeActive) {
1635
+ for (var dk in dayMap) {
1636
+ var dv = dayMap[dk];
1637
+ if (dv && inRangeKey(dv.day)) monthRows.push(dv);
1638
+ }
1639
+ } else {
1640
+ for (var dd = 1; dd <= nDays; dd++) {
1641
+ var kd = y + "-" + pad2(m + 1) + "-" + pad2(dd);
1642
+ if (dayMap[kd]) monthRows.push(dayMap[kd]);
1643
+ }
1644
+ }
1645
+ monthRows.sort(function (a, b) { return a.day < b.day ? 1 : a.day > b.day ? -1 : 0; });
1646
+ // 范围激活时:顶部卡片汇总也按范围内所有天重新计算(覆盖当前月汇总)
1647
+ if (rangeActive) {
1648
+ monthCalls = 0; monthMiss = 0; monthHit = 0; monthOut = 0; monthPeak = 0; monthOff = 0; monthPeakCalls = 0; monthOffCalls = 0;
1649
+ for (var dk2 = 0; dk2 < monthRows.length; dk2++) {
1650
+ var dv2 = monthRows[dk2];
1651
+ monthCalls += dv2.calls; monthMiss += dv2.miss; monthHit += dv2.hit; monthOut += dv2.out;
1652
+ monthPeakCalls += dv2.peakCalls || 0; monthOffCalls += dv2.offPeakCalls || 0;
1653
+ var dsx = daySplit(dv2, regime, splitMap[dv2.day]);
1654
+ monthPeak += dsx.peak; monthOff += dsx.off;
1655
+ }
1656
+ monthCost = monthPeak + monthOff;
1657
+ }
1658
+
1659
+ function prevMonth() { setYm({ y: m === 0 ? y - 1 : y, m: m === 0 ? 11 : m - 1 }); setSelectedDay(null); }
1660
+ function nextMonth() { setYm({ y: m === 11 ? y + 1 : y, m: m === 11 ? 0 : m + 1 }); setSelectedDay(null); }
1661
+ function goToday() { setYm({ y: nowY, m: nowM }); setSelectedDay(bjKey(Date.now())); }
1662
+
1663
+ function presetBtn(k, label) {
1664
+ return el("button", { style: preset === k ? st.segBtnOn : st.segBtn, onClick: function () { setPreset(k); if (k !== "custom") { setFromDate(""); setToDate(""); } } }, label);
1665
+ }
1666
+ function setCustomFrom(v) { setFromDate(v); setPreset("custom"); }
1667
+ function setCustomTo(v) { setToDate(v); setPreset("custom"); }
1668
+ function rangeHintText() {
1669
+ var from = fromDate, to = toDate;
1670
+ if (preset === "today") { from = nowKey; to = nowKey; }
1671
+ else if (preset === "7d") { from = bjKey(bjStartMs(nowKey) - 6 * 86400000); to = nowKey; }
1672
+ else if (preset === "30d") { from = bjKey(bjStartMs(nowKey) - 29 * 86400000); to = nowKey; }
1673
+ return (from ? dayLabel(from) : "…") + " – " + (to ? dayLabel(to) : "…");
1674
+ }
1675
+
1676
+ return el("div", null,
1677
+ el("div", { style: st.calBar },
1678
+ el("button", { style: st.btn, onClick: prevMonth }, t("‹ 上月")),
1679
+ el("span", { style: { fontSize: fs(FS.lg), fontWeight: 600, minWidth: 120, textAlign: "center" } }, fmtMonthLabel(y, m + 1)),
1680
+ el("button", { style: st.btn, onClick: nextMonth }, t("下月 ›")),
1681
+ el("button", { style: st.btn, onClick: goToday }, t("今天")),
1682
+ el("div", { style: st.seg, marginLeft: 8 },
1683
+ el("button", { style: dimMode === "cost" ? st.segBtnOn : st.segBtn, onClick: function () { setDim("cost"); } }, t("按消耗")),
1684
+ el("button", { style: dimMode === "calls" ? st.segBtnOn : st.segBtn, onClick: function () { setDim("calls"); } }, t("按调用数"))
1685
+ )
1686
+ ),
1687
+ el("div", { style: st.calBar },
1688
+ el("div", { style: st.seg },
1689
+ presetBtn("today", t("今天")), presetBtn("7d", t("近7天")), presetBtn("30d", t("近30天")), presetBtn("all", t("全部"))
1690
+ ),
1691
+ el("span", { style: st.note }, t("自定义区间:")),
1692
+ el("input", { type: "date", style: st.dateInput, value: fromDate, onChange: function (e) { setCustomFrom(e.target.value); } }),
1693
+ el("span", { style: st.note }, t("至")),
1694
+ el("input", { type: "date", style: st.dateInput, value: toDate, onChange: function (e) { setCustomTo(e.target.value); } }),
1695
+ (fromDate || toDate || preset !== "all") ? el("button", { style: st.btn, onClick: function () { setFromDate(""); setToDate(""); setPreset("all"); } }, t("清除筛选")) : null
1696
+ ),
1697
+ el("div", { style: st.calGrid },
1698
+ weeks.map(function (w) { return el("div", { key: w, style: st.calWk }, w); }),
1699
+ cells
1700
+ ),
1701
+ el("div", { style: st.legend },
1702
+ el("span", null, t("热力:")),
1703
+ el("span", { style: st.legendBar }),
1704
+ el("span", null, t("低 → 高(") + (dimMode === "cost" ? t("当日消耗") : t("当日调用数")) + ")"),
1705
+ el("span", null, t("· 悬停查看详情,点击某天查看当日调用"))
1706
+ ),
1707
+ el("div", { style: st.cards, marginTop: 12 },
1708
+ el(Card, { label: rangeActive ? t("范围内调用") : t("本月调用"), value: fmtInt(monthCalls), hint: t("高峰 ") + fmtInt(monthPeakCalls) + t(" · 空闲 ") + fmtInt(monthOffCalls) + (rangeActive ? " · " + rangeHintText() : " · " + fmtMonthLabel(y, m + 1)) }),
1709
+ el(Card, { label: t("输入 · 未命中"), value: fmtInt(monthMiss), hint: "token" }),
1710
+ el(Card, { label: t("输入 · 缓存命中"), value: fmtInt(monthHit), hint: "token" }),
1711
+ el(Card, { label: t("输出"), value: fmtInt(monthOut), hint: "token" }),
1712
+ el(Card, { label: t("高峰消耗"), value: fmtMoney(monthPeak), hint: t("工作日高峰 9:00–12:00、14:00–18:00 · 周末全天空闲价") }),
1713
+ el(Card, { label: t("空闲消耗"), value: fmtMoney(monthOff), hint: t("其余空闲时段") }),
1714
+ el(Card, { label: rangeActive ? t("范围内消耗") : t("本月消耗"), value: fmtMoney(monthCost), hint: t("高峰 + 空闲 · ") + costHint(regime) })
1715
+ ),
1716
+ el("div", { style: st.sec }, rangeActive
1717
+ ? (__LANG === "en" ? "Daily stats (" + rangeHintText() + ")" : t("每日统计(") + rangeHintText() + t(")"))
1718
+ : dailyStatsTitle(y, m + 1)),
1719
+ monthRows.length === 0
1720
+ ? el("div", { style: st.empty }, rangeActive ? t("该范围内暂无记录。") : t("本月暂无记录。"))
1721
+ : el("div", { style: st.scroll },
1722
+ el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
1723
+ el("thead", null, el("tr", null,
1724
+ el("th", { style: st.thFirst }, t("日期")), el("th", { style: st.th }, t("调用")), el("th", { style: st.th }, t("高峰/空闲")), el("th", { style: st.th }, t("输入·未命中")), el("th", { style: st.th }, t("缓存命中")), el("th", { style: st.th }, t("输出")), el("th", { style: st.th }, t("高峰消耗")), el("th", { style: st.th }, t("空闲消耗")), el("th", { style: st.th }, t("总消耗(自动)"))
1725
+ )),
1726
+ el("tbody", null, monthRows.map(function (drow) {
1727
+ var sel = selectedDay === drow.day;
1728
+ var ds4 = daySplit(drow, regime, splitMap[drow.day]);
1729
+ return el("tr", { key: drow.day },
1730
+ el("td", { style: sel ? st.tdClickSel : st.tdClick, onClick: (function (k) { return function () { setSelectedDay(selectedDay === k ? null : k); }; })(drow.day) }, dayLabel(drow.day)),
1731
+ el("td", { style: st.td }, fmtInt(drow.calls)),
1732
+ el("td", { style: st.td }, el("span", { style: st.badgePeak }, drow.peakCalls) , " / ", el("span", { style: st.badgeValley }, drow.offPeakCalls)),
1733
+ el("td", { style: st.td }, fmtInt(drow.miss)),
1734
+ el("td", { style: st.td }, el("span", { style: st.badgeHit }, fmtInt(drow.hit))),
1735
+ el("td", { style: st.td }, fmtInt(drow.out)),
1736
+ el("td", { style: st.td }, fmtMoney(ds4.peak)),
1737
+ el("td", { style: st.td }, fmtMoney(ds4.off)),
1738
+ el("td", { style: st.td }, fmtMoney(ds4.peak + ds4.off))
1739
+ );
1740
+ }))
1741
+ )
1742
+ ),
1743
+ selectedDay
1744
+ ? el("div", null,
1745
+ el("div", { style: st.selDayTitle }, dayLabel(selectedDay) + t(" 调用明细")),
1746
+ el("div", { style: st.note, marginTop: 6 },
1747
+ t("高峰 ") + selPeakCalls + t(" 条 · ") + fmtMoney(selPeakCost) +
1748
+ t(" | 空闲 ") + selOffCalls + t(" 条 · ") + fmtMoney(selOffCost) +
1749
+ t(" | 合计 ") + fmtMoney(selPeakCost + selOffCost) + "(" + costHint(regime) + ")"),
1750
+ selRecords.length === 0
1751
+ ? el("div", { style: st.empty }, t("该日无记录。"))
1752
+ : el("div", { style: st.scroll },
1753
+ el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
1754
+ el("thead", null, el("tr", null,
1755
+ el("th", { style: st.thFirst }, t("时间(北京)")), el("th", { style: st.thFirst }, t("模型")), el("th", { style: st.th }, t("输入·未命中")), el("th", { style: st.th }, t("缓存命中")), el("th", { style: st.th }, t("输出")), el("th", { style: st.th }, t("推理")), el("th", { style: st.th }, t("时段")), el("th", { style: st.th }, t("结束")), el("th", { style: st.th }, t("消耗"))
1756
+ )),
1757
+ el("tbody", null, selRecords.map(function (rr) {
1758
+ var hit2 = rr.cacheReadTokens || 0, miss2 = rr.inputTokens || 0;
1759
+ var cost2 = costOf(rr, regime);
1760
+ var int2 = !!rr.interrupted;
1761
+ return el("tr", { key: rr.time },
1762
+ el("td", { style: st.tdFirst }, fmtTime(rr.time)),
1763
+ el("td", { style: st.tdWrap },
1764
+ el("div", null, modelName(rr), int2 ? el("span", { style: st.badgeWarn, marginLeft: 6 }, t("中断")) : null),
1765
+ el("div", { style: st.sub }, providerName(rr.provider))
1766
+ ),
1767
+ el("td", { style: st.td }, fmtInt(miss2)),
1768
+ el("td", { style: st.td }, el("span", { style: st.badgeHit }, fmtInt(hit2))),
1769
+ el("td", { style: st.td }, fmtInt(rr.outputTokens || 0)),
1770
+ el("td", { style: st.td }, rr.reasoningTokens ? fmtInt(rr.reasoningTokens) : "—"),
1771
+ el("td", { style: st.td }, rr.peak ? el("span", { style: st.badgePeak }, t("峰")) : el("span", { style: st.badgeValley }, t("谷"))),
1772
+ el("td", { style: st.td }, int2 ? el("span", { style: st.badgeWarn }, finishLabel(rr.finishReason)) : finishLabel(rr.finishReason)),
1773
+ el("td", { style: st.td }, int2 ? el("span", { style: st.costOff }, "—") : el("span", { style: rr.peak ? st.costPeak : st.costOff }, fmtMoney(cost2)))
1774
+ );
1775
+ }))
1776
+ )
1777
+ )
1778
+ )
1779
+ : null
1780
+ );
1781
+ }
1782
+
1783
+ // ── Cache hit list view (newest first + time filters) ──
1784
+ function CacheListView(props) {
1785
+ var records = props.records, regime = props.regime;
1786
+ var presetState = React.useState("all");
1787
+ var preset = presetState[0], setPreset = presetState[1];
1788
+ var fromState = React.useState("");
1789
+ var fromDate = fromState[0], setFromDate = fromState[1];
1790
+ var toState = React.useState("");
1791
+ var toDate = toState[0], setToDate = toState[1];
1792
+ var pageState = React.useState(1);
1793
+ var page = pageState[0], setPage = pageState[1];
1794
+ var queryState = React.useState("");
1795
+ var query = queryState[0], setQuery = queryState[1];
1796
+
1797
+ var nowKey = bjKey(Date.now());
1798
+ var fromMs = null, toMs = null;
1799
+ if (preset === "today") { fromMs = bjStartMs(nowKey); toMs = Date.now(); }
1800
+ else if (preset === "7d") { fromMs = bjStartMs(nowKey) - 6 * 86400000; toMs = Date.now(); }
1801
+ else if (preset === "30d") { fromMs = bjStartMs(nowKey) - 29 * 86400000; toMs = Date.now(); }
1802
+ else if (preset === "custom") {
1803
+ if (fromDate) fromMs = bjStartMs(fromDate);
1804
+ if (toDate) toMs = bjStartMs(toDate) + 86400000 - 1;
1805
+ }
1806
+
1807
+ var q = String(query || "").trim().toLowerCase();
1808
+ var filtered = [];
1809
+ for (var i = 0; i < records.length; i++) {
1810
+ var r = records[i];
1811
+ if (fromMs != null && r.time < fromMs) continue;
1812
+ if (toMs != null && r.time > toMs) continue;
1813
+ if (q && String(r.model || "").toLowerCase().indexOf(q) < 0
1814
+ && String(r.provider || "").toLowerCase().indexOf(q) < 0
1815
+ && String(r.sessionId || "").toLowerCase().indexOf(q) < 0
1816
+ && String(r.purpose || "").toLowerCase().indexOf(q) < 0) continue;
1817
+ filtered.push(r);
1818
+ }
1819
+ filtered.sort(function (a, b) { return b.time - a.time; }); // newest first
1820
+
1821
+ var sumHit = 0, sumMiss = 0, sumOut = 0, sumCost = 0, sumWrite = 0, sumReason = 0;
1822
+ for (var s = 0; s < filtered.length; s++) {
1823
+ var rr2 = filtered[s];
1824
+ sumHit += rr2.cacheReadTokens || 0; sumMiss += rr2.inputTokens || 0; sumOut += rr2.outputTokens || 0;
1825
+ sumWrite += rr2.cacheWriteTokens || 0; sumReason += rr2.reasoningTokens || 0;
1826
+ sumCost += costOf(rr2, regime);
1827
+ }
1828
+
1829
+ // 分页渲染:只渲染当前页,避免上千行 DOM 导致卡顿
1830
+ var PAGE_SIZE = 100;
1831
+ var totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
1832
+ var curPage = Math.min(Math.max(1, page), totalPages);
1833
+ var pageRows = filtered.slice((curPage - 1) * PAGE_SIZE, curPage * PAGE_SIZE);
1834
+
1835
+ function setCustomFrom(v) { setFromDate(v); setPreset("custom"); setPage(1); }
1836
+ function setCustomTo(v) { setToDate(v); setPreset("custom"); setPage(1); }
1837
+ function presetBtn(k, label) {
1838
+ return el("button", { style: preset === k ? st.segBtnOn : st.segBtn, onClick: function () { setPreset(k); setPage(1); } }, label);
1839
+ }
1840
+ function pageBtn(kind, label, disabled, onClick) {
1841
+ return el("button", { style: disabled ? st.btnDisabled : st.btn, onClick: onClick }, label);
1842
+ }
1843
+ // 分页切换条:顶部与底部各渲染一份,便于长列表翻页。
1844
+ function pagerBar() {
1845
+ return el("div", { style: st.calBar },
1846
+ pageBtn("prev", t("‹ 上一页"), curPage <= 1, function () { setPage(curPage - 1); }),
1847
+ el("span", { style: st.note }, t("第 ") + curPage + " / " + totalPages + t(" 页 · 每页 ") + PAGE_SIZE + t(" 条 · 共 ") + fmtInt(filtered.length) + t(" 条")),
1848
+ pageBtn("next", t("下一页 ›"), curPage >= totalPages, function () { setPage(curPage + 1); })
1849
+ );
1850
+ }
1851
+ var sums = splitTotals(filtered, regime);
1852
+
1853
+ return el("div", null,
1854
+ el("div", { style: st.calBar },
1855
+ el("div", { style: st.seg },
1856
+ presetBtn("today", t("今天")), presetBtn("7d", t("近7天")), presetBtn("30d", t("近30天")), presetBtn("all", t("全部"))
1857
+ ),
1858
+ el("span", { style: st.note }, t("自定义区间:")),
1859
+ el("input", { type: "date", style: st.dateInput, value: fromDate, onChange: function (e) { setCustomFrom(e.target.value); } }),
1860
+ el("span", { style: st.note }, t("至")),
1861
+ el("input", { type: "date", style: st.dateInput, value: toDate, onChange: function (e) { setCustomTo(e.target.value); } }),
1862
+ (fromDate || toDate) ? el("button", { style: st.btn, onClick: function () { setFromDate(""); setToDate(""); setPreset("all"); setPage(1); } }, t("清除区间")) : null
1863
+ ),
1864
+ el("div", { style: st.calBar },
1865
+ 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); } }),
1866
+ query ? el("button", { style: st.btn, onClick: function () { setQuery(""); setPage(1); } }, t("清除")) : null
1867
+ ),
1868
+ el("div", { style: st.note, marginTop: 8 },
1869
+ t("当前范围:共 ") + fmtInt(filtered.length) + t(" 条 · 输入·未命中 ") + fmtInt(sumMiss) + t(" · 缓存命中 ") + fmtInt(sumHit) + t(" · 缓存写入 ") + fmtInt(sumWrite) + t(" · 输出 ") + fmtInt(sumOut) +
1870
+ t(" · 高峰消耗 ") + fmtMoney(sums.peak) + t(" · 空闲消耗 ") + fmtMoney(sums.off) + t(" · 总消耗 ") + fmtMoney(sumCost) + "(" + costHint(regime) + ")"
1871
+ ),
1872
+ filtered.length === 0
1873
+ ? el("div", { style: st.empty }, query || (fromDate || toDate || preset !== "all") ? t("没有符合条件的记录。") : t("该时间范围内没有记录。"))
1874
+ : el("div", null,
1875
+ pagerBar(),
1876
+ el("div", { style: st.scroll },
1877
+ el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
1878
+ el("thead", null, el("tr", null,
1879
+ el("th", { style: st.thFirst }, t("时间(北京)")), el("th", { style: st.thFirst }, t("模型")), el("th", { style: st.th }, t("输入·未命中")), el("th", { style: st.th }, t("缓存命中")), el("th", { style: st.th }, t("缓存写入")), el("th", { style: st.th }, t("输出")), el("th", { style: st.th }, t("推理")), el("th", { style: st.th }, t("命中率")), el("th", { style: st.th }, t("时段")), el("th", { style: st.th }, t("结束")), el("th", { style: st.th }, t("消耗"))
1880
+ )),
1881
+ el("tbody", null,
1882
+ pageRows.map(function (rr3, idx) {
1883
+ var hit3 = rr3.cacheReadTokens || 0, miss3 = rr3.inputTokens || 0;
1884
+ var cost3 = costOf(rr3, regime);
1885
+ var int3 = !!rr3.interrupted;
1886
+ return el("tr", { key: rr3.time + "-" + idx },
1887
+ el("td", { style: st.tdFirst }, fmtTime(rr3.time)),
1888
+ el("td", { style: st.tdWrap },
1889
+ el("div", null, modelName(rr3), int3 ? el("span", { style: st.badgeWarn, marginLeft: 6 }, t("中断")) : null),
1890
+ el("div", { style: st.sub }, (rr3.provider || "") + (rr3.purpose ? " · " + rr3.purpose : ""))
1891
+ ),
1892
+ el("td", { style: st.td }, fmtInt(miss3)),
1893
+ el("td", { style: st.td }, el("span", { style: st.badgeHit }, fmtInt(hit3))),
1894
+ el("td", { style: st.td }, rr3.cacheWriteTokens ? fmtInt(rr3.cacheWriteTokens) : "—"),
1895
+ el("td", { style: st.td }, fmtInt(rr3.outputTokens || 0)),
1896
+ el("td", { style: st.td }, rr3.reasoningTokens ? fmtInt(rr3.reasoningTokens) : "—"),
1897
+ el("td", { style: st.td }, pct(hit3, hit3 + miss3)),
1898
+ el("td", { style: st.td }, rr3.peak ? el("span", { style: st.badgePeak }, t("峰")) : el("span", { style: st.badgeValley }, t("谷"))),
1899
+ el("td", { style: st.td }, int3 ? el("span", { style: st.badgeWarn }, finishLabel(rr3.finishReason)) : finishLabel(rr3.finishReason)),
1900
+ el("td", { style: st.td }, int3 ? el("span", { style: st.costOff }, "—") : el("span", { style: rr3.peak ? st.costPeak : st.costOff }, fmtMoney(cost3)))
1901
+ );
1902
+ }),
1903
+ el("tr", null,
1904
+ el("td", { style: st.tdTotalFirst }, t("总费用合计")),
1905
+ el("td", { style: st.tdTotal }, fmtInt(filtered.length) + t(" 条")),
1906
+ el("td", { style: st.tdTotal }, fmtInt(sumMiss)),
1907
+ el("td", { style: st.tdTotal }, fmtInt(sumHit)),
1908
+ el("td", { style: st.tdTotal }, fmtInt(sumWrite)),
1909
+ el("td", { style: st.tdTotal }, fmtInt(sumOut)),
1910
+ el("td", { style: st.tdTotal }, sumReason ? fmtInt(sumReason) : "—"),
1911
+ el("td", { style: st.tdTotal }, pct(sumHit, sumHit + sumMiss)),
1912
+ el("td", { style: st.tdTotal }, "—"),
1913
+ el("td", { style: st.tdTotal }, "—"),
1914
+ el("td", { style: st.tdTotal }, fmtMoney(sumCost))
1915
+ )
1916
+ )
1917
+ )
1918
+ ),
1919
+ pagerBar()
1920
+ )
1921
+ );
1922
+ }
1923
+
1924
+ // ── Price table view ──
1925
+ // 价格表按官方固定,不可编辑。顶部可切换查看「自动 / 峰谷价 / 基础价」三档;
1926
+ // 自动档展示当前生效价格(生效前=基础价,生效后=当前峰谷价表);
1927
+ // 峰谷价表本身也按官方调价分段(2026-08-17 第一版 → 2026-09-10 12:00 V4.1 新版)。
1928
+ function PriceView(props) {
1929
+ var pricing = props.pricing || { base: {}, peakValley: {}, peakValleyV41: {} };
1930
+ var effectiveAt = props.effectiveAt || 0;
1931
+ var effectiveV41At = props.effectiveV41At || 0;
1932
+ var fx = props.fx || {};
1933
+ var regimeState = React.useState("auto");
1934
+ var regime = regimeState[0], setRegime = regimeState[1];
1935
+ // 当前生效的峰谷价表:DeepSeek V4.1 Flash 调价(2026-09-10 12:00 北京时间)前
1936
+ // 为第一版峰谷价,调价后为新版(deepseek-flash 单价下调,deepseek-v4-pro 不变)。
1937
+ var currentPvTable = function () {
1938
+ return (effectiveV41At && Date.now() >= effectiveV41At ? pricing.peakValleyV41 : pricing.peakValley) || {};
1939
+ };
1940
+ var effectiveIn = function () {
1941
+ if (!effectiveAt) return t("新价格表生效时间未知");
1942
+ var now = Date.now();
1943
+ var s = now < effectiveAt
1944
+ ? t("新价格表(峰谷价)将于 ") + fmtTime(effectiveAt) + t("(北京时间)生效,当前按旧价格表(基础价)计费")
1945
+ : t("新价格表(峰谷价)已生效(自 ") + fmtTime(effectiveAt) + t(" 起)");
1946
+ if (effectiveV41At) {
1947
+ s += " " + (now < effectiveV41At
1948
+ ? t("DeepSeek V4.1 Flash 调价将于 ") + fmtTime(effectiveV41At) + t("(北京时间)生效:deepseek-flash 改用新版峰谷价(空闲 缓存命中 0.02 / 输入 1 / 输出 4 元,高峰为其两倍),deepseek-v4-pro 价格不变。")
1949
+ : t("DeepSeek V4.1 Flash 调价已生效(自 ") + fmtTime(effectiveV41At) + t(" 起):deepseek-flash 按新版峰谷价计费,deepseek-v4-pro 价格不变。"));
1950
+ }
1951
+ return s;
1952
+ };
1953
+ var showRegime = function (r) {
1954
+ if (r === "auto") return Date.now() < effectiveAt ? "base" : "peakValley";
1955
+ return r;
1956
+ };
1957
+ var shown = showRegime(regime);
1958
+ // auto 档:显示当前生效的价格表(生效前=基础价 base,生效后=当前峰谷价表)
1959
+ var table = shown === "base" ? (pricing.base || {}) : currentPvTable();
1960
+ var cell = function (mk, a, b) {
1961
+ var row = table && table[mk];
1962
+ if (!row) return "—";
1963
+ if (shown === "base") return fmtPrice(row[a]);
1964
+ var sub = row[a];
1965
+ return sub ? fmtPrice(sub[b]) : "—";
1966
+ };
1967
+ var rows = PRICE_MODELS.map(function (mk) {
1968
+ if (shown === "base") {
1969
+ return el("tr", { key: mk },
1970
+ el("td", { style: st.tdFirst }, modelLabel(mk)),
1971
+ el("td", { style: st.td }, el("span", { style: st.badgeHit }, cell(mk, "cacheHit"))),
1972
+ el("td", { style: st.td }, cell(mk, "cacheMiss")),
1973
+ el("td", { style: st.td }, cell(mk, "output"))
1974
+ );
1975
+ }
1976
+ return el("tr", { key: mk },
1977
+ el("td", { style: st.tdFirst }, modelLabel(mk)),
1978
+ el("td", { style: st.td }, el("span", { style: st.badgeValley }, cell(mk, "offPeak", "cacheHit"))),
1979
+ el("td", { style: st.td }, cell(mk, "offPeak", "cacheMiss")),
1980
+ el("td", { style: st.td }, cell(mk, "offPeak", "output")),
1981
+ el("td", { style: st.td }, el("span", { style: st.badgePeak }, cell(mk, "peak", "cacheHit"))),
1982
+ el("td", { style: st.td }, cell(mk, "peak", "cacheMiss")),
1983
+ el("td", { style: st.td }, cell(mk, "peak", "output"))
1984
+ );
1985
+ });
1986
+
1987
+ var budget = props.budget || null;
1988
+ var budgetInputState = React.useState("");
1989
+ var budgetInput = budgetInputState[0], setBudgetInput = budgetInputState[1];
1990
+ var budgetMsgState = React.useState("");
1991
+ var budgetMsg = budgetMsgState[0], setBudgetMsg = budgetMsgState[1];
1992
+ function doSaveBudget() {
1993
+ var v = String(budgetInput || "").trim();
1994
+ if (!v) { setBudgetMsg(t("请输入预算金额")); return; }
1995
+ var n = Number(v);
1996
+ if (!Number.isFinite(n) || n < 0) { setBudgetMsg(t("预算金额必须是非负数字")); return; }
1997
+ setBudgetMsg(""); setBudgetInput("");
1998
+ if (props.onBudgetChange) props.onBudgetChange(n);
1999
+ }
2000
+ function doClearBudget() {
2001
+ setBudgetInput(""); setBudgetMsg("");
2002
+ if (props.onBudgetChange) props.onBudgetChange(null);
2003
+ }
2004
+ var repriceMsgState = React.useState("");
2005
+ var repriceMsg = repriceMsgState[0], setRepriceMsg = repriceMsgState[1];
2006
+ var histState = React.useState(false);
2007
+ var histOpen = histState[0], setHistOpen = histState[1];
2008
+ // 价格口径说明 + 第三方价格表:查证时才看,默认折叠
2009
+ var calState = React.useState(false);
2010
+ var caliberOpen = calState[0], setCaliberOpen = calState[1];
2011
+ var pricingHistory = props.pricingHistory || [];
2012
+ var pricingMeta = props.pricingMeta || null;
2013
+ function doRepriceAll() {
2014
+ if (!props.onRepriceAll) { setRepriceMsg(t("当前宿主不支持重算")); return; }
2015
+ setRepriceMsg(t("已提交,正在用当前价格表重算全部历史…"));
2016
+ props.onRepriceAll();
2017
+ }
2018
+ return el("div", null,
2019
+ el("div", { style: Object.assign({}, st.infobox, { marginTop: 0 }) },
2020
+ el("div", { style: st.infoboxTitle }, t("月度预算")),
2021
+ el("div", { style: st.note }, budget && budget.monthly > 0
2022
+ ? t("当前预算:") + fmtMoney(budget.monthly) + t(";概览页会按当月消耗显示进度与超支预警。")
2023
+ : t("设置每月消费上限,概览页会显示进度与超支预警。")),
2024
+ el("div", { style: st.actions, marginTop: 8 },
2025
+ el("input", { style: Object.assign({}, st.numInput, { width: 130, textAlign: "left" }), placeholder: t("预算金额(元)"), value: budgetInput, onChange: function (e) { setBudgetInput(e.target.value); } }),
2026
+ el("button", { style: st.btnPrimary, onClick: doSaveBudget }, t("保存预算")),
2027
+ budget && budget.monthly > 0 ? el("button", { style: st.btn, onClick: doClearBudget }, t("清除预算")) : null,
2028
+ budgetMsg ? el("span", { style: st.err }, budgetMsg) : null
2029
+ )
2030
+ ),
2031
+ el("div", { style: st.infobox },
2032
+ el("div", { style: st.infoboxTitle }, t("历史价格与重算")),
2033
+ el("div", { style: st.note }, props.legacyPricingCount > 0
2034
+ ? t("有 ") + fmtInt(props.legacyPricingCount) + t(" 条记录没有冻结单价(旧版本写入或手工导入),其费用按当前价格表计算,改动价格后会跟着变化。")
2035
+ : t("所有记录都已冻结写入时生效的单价:改动价格表只影响之后产生的记录,历史费用不会被改写。")),
2036
+ el("div", { style: st.actions, marginTop: 8 },
2037
+ el("button", { style: st.btn, onClick: doRepriceAll }, t("用当前价格表重算全部历史")),
2038
+ repriceMsg ? el("span", { style: st.sub }, repriceMsg) : null
2039
+ )
2040
+ ),
2041
+ // 价格来源与历史快照:官方页面每次核对后把结论落在这里,避免"价格为什么变了"
2042
+ // 只能靠猜。历史快照折叠展示,默认不占版面。
2043
+ el("div", { style: st.infobox },
2044
+ el("div", { style: st.infoboxTitle }, t("价格来源与快照")),
2045
+ el("div", { style: st.note }, t("官方价目页核对日期:") + (pricingMeta && pricingMeta.checkedAt ? pricingMeta.checkedAt : "—")),
2046
+ pricingMeta
2047
+ ? el("div", { style: st.note }, t("新增 Search / Fetch 按次计费 ¥5 / 1000 次成功请求:本插件拿不到该调用次数,未计入费用。"))
2048
+ : null,
2049
+ pricingMeta
2050
+ ? el("div", { style: st.note }, t("法定节假日已纳入峰谷判定(自 ") + fmtTime(pricingMeta.holidayRuleAt) + t(" 起生效)。"))
2051
+ : null,
2052
+ el("div", { style: st.actions, marginTop: 8 },
2053
+ el("button", {
2054
+ style: st.btn,
2055
+ onClick: function () { setHistOpen(!histOpen); }
2056
+ }, (histOpen ? t("收起") : t("展开")) + t("历史价格记录(") + fmtInt(pricingHistory.length) + t(" 条)")),
2057
+ pricingMeta && pricingMeta.sourceUrl
2058
+ ? el("a", { style: st.actionLink, href: pricingMeta.sourceUrl, target: "_blank", rel: "noreferrer" }, t("核对官方价格页"))
2059
+ : null
2060
+ ),
2061
+ histOpen
2062
+ ? el("div", { style: { marginTop: 8 } },
2063
+ pricingHistory.length === 0
2064
+ ? el("div", { style: st.note }, t("暂无历史快照;首次改价后会自动记录。"))
2065
+ : pricingHistory.slice().reverse().map(function (h, i) {
2066
+ var table = h && h.table ? h.table : {};
2067
+ var row = table.peakValleyV41 && table.peakValleyV41["deepseek-flash"] ? table.peakValleyV41["deepseek-flash"].offPeak : null;
2068
+ return el("div", { key: "hist" + String(i), style: st.note },
2069
+ fmtTime(h.at) + t(" · ") + String(h.label || h.kind || "")
2070
+ + (row ? t(" · flash 空闲价 ") + fmtMoney(row.cacheHit) + " / " + fmtMoney(row.cacheMiss) + " / " + fmtMoney(row.output) : ""));
2071
+ })
2072
+ )
2073
+ : null
2074
+ ),
2075
+ el("div", { style: st.nowPeriod },
2076
+ el("span", { style: periodNow().peak ? st.badgePeak : st.badgeValley }, t("当前 · ") + t(periodNow().label)),
2077
+ el("span", { style: st.sub }, t(periodNow().desc))
2078
+ ),
2079
+ el("div", { style: st.sec }, t("今日 $ / ¥ 汇率")),
2080
+ el("div", { style: st.cards, marginTop: 8 },
2081
+ el(Card, { label: "$ → ¥", value: fx.rate ? ("$1 = ¥" + Number(fx.rate).toFixed(4)) : t("暂不可用"), hint: fx.date ? (t("汇率日期 ") + fx.date) : t("等待获取") }),
2082
+ el(Card, { label: "¥ → $", value: fx.inverse ? ("¥1 = $" + Number(fx.inverse).toFixed(6)) : t("暂不可用"), hint: fx.source || "Frankfurter" }),
2083
+ el(Card, { label: t("汇率状态"), value: fx.stale ? t("缓存汇率") : (fx.rate ? t("最新可用") : t("获取失败")), hint: fx.error || "Frankfurter · central-bank reference rates" })
2084
+ ),
2085
+ el("div", { style: st.actions, marginTop: 8 },
2086
+ el("button", { style: st.btn, onClick: props.onFxRefresh }, t("刷新汇率")),
2087
+ el("span", { style: st.note }, t("美元计价的 DigitalOcean 消耗按该 USD/CNY 汇率换算成人民币;人民币定价不受汇率影响。"))
2088
+ ),
2089
+ el("div", { style: st.sec, marginTop: 18 }, t("DeepSeek 官方 API 价格表")),
2090
+ el("div", { style: st.note, marginTop: 6 },
2091
+ t("本表仅用于 DeepSeek 官方 Provider;第三方 Provider 按下方已核验的自身价格计费,无法可靠匹配价格的模型按 0 统计。价格按官方公布固定,不可编辑。")),
2092
+ el("div", { style: st.calBar },
2093
+ el("div", { style: st.seg },
2094
+ el("button", { style: regime === "auto" ? st.segBtnOn : st.segBtn, onClick: function () { setRegime("auto"); } }, t("自动")),
2095
+ el("button", { style: regime === "peakValley" ? st.segBtnOn : st.segBtn, onClick: function () { setRegime("peakValley"); } }, t("峰谷价")),
2096
+ el("button", { style: regime === "base" ? st.segBtnOn : st.segBtn, onClick: function () { setRegime("base"); } }, t("基础价"))
2097
+ )
2098
+ ),
2099
+ el("div", { style: st.note, marginTop: 6 }, effectiveIn()),
2100
+ regime === "auto"
2101
+ ? el("div", { style: st.note, marginTop: 6 },
2102
+ t("自动模式:按调用时间自动选择计费档位——新价格表生效前的调用按基础价(旧价格表),生效后的调用按峰谷价(工作日高峰时段 9:00–12:00、14:00–18:00 用高峰价,其余时间——含周末全天——用空闲价)。当前显示:"),
2103
+ el("strong", null, shown === "base" ? t("基础价表(旧价格表,新价格生效前)") : t("峰谷价表(新价格表)")),
2104
+ t("。价格按官方公布固定,不可编辑。")
2105
+ )
2106
+ : null,
2107
+ shown === "base"
2108
+ ? el("div", { style: st.scroll },
2109
+ el("table", { className: "dsh-usage-table", style: st.tbl },
2110
+ el("thead", null, el("tr", null,
2111
+ el("th", { style: st.thFirst }, t("模型")), el("th", { style: st.th }, t("缓存命中(输入)")), el("th", { style: st.th }, t("输入 · 未命中")), el("th", { style: st.th }, t("输出"))
2112
+ )),
2113
+ el("tbody", null, rows)
2114
+ )
2115
+ )
2116
+ : el("div", { style: st.scroll },
2117
+ el("table", { className: "dsh-usage-table", style: st.tbl },
2118
+ el("thead", null, el("tr", null,
2119
+ el("th", { style: st.thFirst }, t("模型")),
2120
+ el("th", { style: st.th }, t("空闲 · 缓存命中")), el("th", { style: st.th }, t("空闲 · 输入")), el("th", { style: st.th }, t("空闲 · 输出")),
2121
+ el("th", { style: st.th }, t("高峰 · 缓存命中")), el("th", { style: st.th }, t("高峰 · 输入")), el("th", { style: st.th }, t("高峰 · 输出"))
2122
+ )),
2123
+ el("tbody", null, rows)
2124
+ )
2125
+ ),
2126
+ // 价格口径 + 第三方价格表:都是"查证时才看"的长文案,默认折叠成一行。
2127
+ // 另外原来这里 WhyThisPrice 已经内含 OfficialLinks,却又单独渲染了一遍,
2128
+ // 导致同一组外链出现两次(已去掉重复的那次)。
2129
+ el("div", { style: st.calBar, marginTop: 10 },
2130
+ el("button", { style: caliberOpen ? st.btnPrimary : st.btn, onClick: function () { setCaliberOpen(!caliberOpen); } },
2131
+ caliberOpen ? t("收起说明") : t("价格口径与第三方价格")),
2132
+ el("span", { style: st.note }, t("单位:元 / 百万 tokens"))
2133
+ ),
2134
+ caliberOpen ? el("div", { style: Object.assign({}, st.infobox, { marginTop: 8 }) },
2135
+ el("div", { style: st.note },
2136
+ t("单位:元 / 百万 tokens。工作日高峰时段(北京时间 9:00–12:00、14:00–18:00)用高峰价,其余时间用空闲价(空闲价 = 高峰价的一半);自 2026-08-23 起周末(周六、周日)全天按空闲价计费,新规则生效前仍按原规则。概览、用量日历、缓存命中列表中的消耗均已按高峰 / 空闲分列统计。")
2137
+ ),
2138
+ el("div", { style: st.note, marginTop: 6 },
2139
+ t("峰谷价表按生效时间分段:2026-08-17 00:00 起为第一版峰谷价;2026-09-10 12:00 起为 DeepSeek V4.1 Flash 调价后的新版(deepseek-flash 空闲价下调为 缓存命中 0.02 / 输入 1 / 输出 4 元,高峰为其两倍;deepseek-v4-pro 价格不变)。上方「峰谷价」展示的是当前生效的一版,历史调用始终按其发生时刻对应的版本计费。")
2140
+ ),
2141
+ el("div", { style: st.note, marginTop: 6 },
2142
+ t("旧模型名 deepseek-v4-flash 与 deepseek-v4-flash-vision-exp 仍可调用(官方已将其路由到 DeepSeek-V4.1-Flash、按 Flash 单价计费),在本价格表中与 deepseek-flash 合并为同一档位。")
2143
+ ),
2144
+ el(WhyThisPrice, null),
2145
+ el("div", { style: st.sec, marginTop: 18 }, t("第三方平台价格与覆盖状态(2026-08-19 核验)")),
2146
+ el("div", { style: st.note, marginTop: 6 }, t("只把能与记录中的 provider/model 可靠匹配的价格用于自动计费;美元价格先按 USD 计算,再乘 USD/CNY 汇率统一折算成人民币。")),
2147
+ el("div", { style: st.scroll, marginTop: 8 },
2148
+ el("table", { className: "dsh-usage-table", style: st.tbl },
2149
+ el("thead", null, el("tr", null,
2150
+ el("th", { style: st.thFirst }, t("API 服务商")), el("th", { style: st.thFirst }, t("模型 / 状态")),
2151
+ el("th", { style: st.th }, t("缓存命中")), el("th", { style: st.th }, t("输入")), el("th", { style: st.th }, t("输出")), el("th", { style: st.thFirst }, t("说明"))
2152
+ )),
2153
+ el("tbody", null, [
2154
+ ["SiliconFlow", "DeepSeek-V4-Flash", "¥0.02", "¥1.00", "¥2.00", t("已用于自动计费")],
2155
+ ["SiliconFlow", "DeepSeek-V4-Pro", "¥1.00", "¥12.00", "¥24.00", t("缓存命中价按 2026-08-03 起生效公告")],
2156
+ ["SiliconFlow", "DeepSeek-V3.2", "¥0.40", "¥4.00", "¥6.00", t("已用于自动计费")],
2157
+ ["SiliconFlow", "Qwen3.6-27B", t("按输入价"), "¥3.00", "¥18.00", t("公告未单列缓存命中价")],
2158
+ ["DigitalOcean", "DeepSeek V4 Flash", "$0.028", "$0.112", "$0.224", t("自动按 USD/CNY 汇率换算人民币并计费")],
2159
+ ["DigitalOcean", "DeepSeek V4 Pro", "$0.348", "$1.392", "$2.784", t("自动按 USD/CNY 汇率换算人民币并计费")],
2160
+ ["DigitalOcean", "DeepSeek V3.2", "$0.15", "$0.425", "$1.36", t("自动按 USD/CNY 汇率换算人民币并计费")],
2161
+ [t("Alibaba / 千问"), t("暂不计费"), "—", "—", "—", t("调用与 token 正常统计;费用暂按 ¥0")],
2162
+ ["AMD GPU Cloud", "DeepSeek V4 Flash", "¥0", "¥0", "¥0", t("免费;费用固定按 ¥0")]
2163
+ ].map(function (row, idx) {
2164
+ return el("tr", { key: "provider-price-" + idx },
2165
+ el("td", { style: st.tdFirst }, row[0]), el("td", { style: st.tdWrap }, row[1]),
2166
+ el("td", { style: st.td }, row[2]), el("td", { style: st.td }, row[3]), el("td", { style: st.td }, row[4]), el("td", { style: st.tdWrap }, row[5])
2167
+ );
2168
+ }))
2169
+ )
2170
+ ),
2171
+ el("div", { style: st.note, marginTop: 8 }, t("DigitalOcean 使用美元官方价并按最新可用 USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud 的 DeepSeek V4 Flash 免费按 ¥0。"))
2172
+ ) : null
2173
+ );
2174
+ }
2175
+ // ── Usage panel ──
2176
+ // 新的信息架构:看结论(用量)→ 看时间分布(日历)→ 查明细(明细)→ 改配置(设置)。
2177
+ // 原来的「概览 / 用量日历 / 缓存命中列表 / 价格表」把数据和配置混在同一层,
2178
+ // 且"缓存命中列表"既是明细又是独立页签,职责重叠。
2179
+ var SUBTABS = [
2180
+ { k: "overview", z: "用量" },
2181
+ { k: "calendar", z: "日历" },
2182
+ { k: "detail", z: "明细" },
2183
+ { k: "settings", z: "设置" }
2184
+ ];
2185
+
2186
+ function UsagePanel(props) {
2187
+ var lang = useLang(); // 订阅语言变化:宿主切换语言时触发本面板重渲染(值本身仅作触发用)
2188
+ var state = React.useState({ records: [], count: 0, dataPath: "", persistOk: false, persistError: "", pricing: null, days: [], budget: null });
2189
+ var data = state[0], setData = state[1];
2190
+ var tabState = React.useState("overview");
2191
+ var tab = tabState[0], setTab = tabState[1];
2192
+ var infoState = React.useState(false);
2193
+ var infoOpen = infoState[0], setInfoOpen = infoState[1];
2194
+ var errState = React.useState("");
2195
+ var error = errState[0], setError = errState[1];
2196
+ var expState = React.useState("");
2197
+ var exportMsg = expState[0], setExportMsg = expState[1];
2198
+ var impState = React.useState("");
2199
+ var importMsg = impState[0], setImportMsg = impState[1];
2200
+ var destState = React.useState("");
2201
+ var destDir = destState[0], setDestDir = destState[1];
2202
+
2203
+ var canvasNode = null, fileInputNode = null;
2204
+ var timer = props.timer;
2205
+ // 初始加载中标记:首次请求完成/失败后置 false(后续定时刷新静默进行,避免闪烁)
2206
+ var loadingState = React.useState(true);
2207
+ var loading = loadingState[0], setLoading = loadingState[1];
2208
+ // 上一次已知的数据指纹(记录数 / 最新时刻 / 价格版本)。轮询先比这个,
2209
+ // 没变化就不拉全量 list —— 见 poll()。
2210
+ var statRef = React.useRef(null);
2211
+ if (!statRef.current) statRef.current = { count: -1, latest: 0, pricingVersion: -1 };
2212
+
2213
+ function refresh() {
2214
+ api({ action: "list" }).then(function (res) {
2215
+ var next = res || { records: [], count: 0, dataPath: "", persistOk: false, persistError: "", pricing: null, days: [], budget: null };
2216
+ setData(next);
2217
+ setError("");
2218
+ setLoading(false);
2219
+ statRef.current = { count: next.total, latest: next.latest, pricingVersion: next.pricingVersion };
2220
+ }).catch(function (e) { setError(String((e && e.message) || e)); setLoading(false); });
2221
+ }
2222
+
2223
+ /**
2224
+ * 轮询:先问 `stat`(几十字节),只有记录数 / 最新时刻 / 价格版本变了才拉全量。
2225
+ *
2226
+ * 旧实现每 10 秒直接调 `list`:等于每 10 秒把上万条记录重新投影、序列化、
2227
+ * JSON.parse、再全量重渲染一次面板——这是"用完这个插件就卡"的主因,
2228
+ * 而且数据没变化时也在白跑。
2229
+ */
2230
+ function poll() {
2231
+ try {
2232
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
2233
+ } catch (e) {}
2234
+ api({ action: "stat" }).then(function (res) {
2235
+ if (!res || !res.ok) return;
2236
+ var prev = statRef.current || {};
2237
+ if (prev.count === res.count && prev.latest === res.latest && prev.pricingVersion === res.pricingVersion) return;
2238
+ refresh();
2239
+ }).catch(function () {});
2240
+ }
2241
+
2242
+ function saveBudget(monthly) {
2243
+ api({ action: "setBudget", monthly: monthly }).then(function (res) {
2244
+ if (res && res.ok) setData(function (prev) { return Object.assign({}, prev, { budget: res.budget }); });
2245
+ }).catch(function () {});
2246
+ }
2247
+
2248
+ React.useEffect(function () {
2249
+ refresh();
2250
+ // 10 秒探测一次"有没有新数据",只有变化时才真正拉全量(poll 内部判断)
2251
+ if (timer && timer.interval) return timer.interval(poll, 10000);
2252
+ return undefined;
2253
+ }, []);
2254
+
2255
+ function showExport(res) {
2256
+ if (res && res.ok) {
2257
+ setExportMsg({ k: "已导出:", tail: res.path });
2258
+ if (res.dir) api({ action: "reveal", dir: res.dir }).catch(function () {});
2259
+ } else {
2260
+ setExportMsg({ k: "导出失败:", tail: (res && res.error) || t("未知错误") });
2261
+ }
2262
+ }
2263
+
2264
+ function doExport(kind) {
2265
+ setExportMsg({ k: "导出中…" });
2266
+ api({ action: "export", kind: kind, dir: destDir || undefined }).then(showExport).catch(function (e) { setExportMsg({ k: "导出失败:", tail: String((e && e.message) || e) }); });
2267
+ }
2268
+
2269
+ function doExportImage() {
2270
+ setExportMsg({ k: "生成图片中…" });
2271
+ if (!canvasNode) { setExportMsg({ k: "画布不可用" }); return; }
2272
+ try {
2273
+ drawReport(canvasNode, (data.records || []).slice());
2274
+ var dataUrl = canvasNode.toDataURL("image/png");
2275
+ api({ action: "exportPng", dataUrl: dataUrl, dir: destDir || undefined }).then(showExport).catch(function (e) { setExportMsg({ k: "导出失败:", tail: String((e && e.message) || e) }); });
2276
+ } catch (e) {
2277
+ setExportMsg({ k: "生成图片失败:", tail: String((e && e.message) || e) });
2278
+ }
2279
+ }
2280
+
2281
+ function pickDestDir() {
2282
+ setExportMsg({ k: "打开目录选择…" });
2283
+ api({ action: "pickDir" }).then(function (res) {
2284
+ if (res && res.ok) { setDestDir(res.path); setExportMsg({ k: "导出目标:", tail: res.path }); }
2285
+ else if (res && res.cancelled) { setExportMsg(""); }
2286
+ else { setExportMsg({ k: "选择目录失败:", tail: (res && res.error) || "" }); }
2287
+ }).catch(function (e) { setExportMsg({ k: "选择目录失败:", tail: String((e && e.message) || e) }); });
2288
+ }
2289
+
2290
+ function doReveal() {
2291
+ api({ action: "reveal", dir: destDir || "data" }).then(function (res) {
2292
+ if (!res || !res.ok) setExportMsg({ k: "打开文件夹失败:", tail: (res && res.error) || "" });
2293
+ }).catch(function (e) { setExportMsg({ k: "打开文件夹失败:", tail: String((e && e.message) || e) }); });
2294
+ }
2295
+
2296
+ function doClear() {
2297
+ api({ action: "clear" }).then(refresh).catch(function (e) { setError(String((e && e.message) || e)); });
2298
+ }
2299
+
2300
+ function pickFile() { if (fileInputNode) fileInputNode.click(); }
2301
+
2302
+ function onFileChange(e) {
2303
+ var f = e.target.files && e.target.files[0];
2304
+ if (!f) return;
2305
+ setImportMsg({ k: "读取文件…" });
2306
+ var readPromise = typeof f.text === "function"
2307
+ ? f.text()
2308
+ : new Promise(function (resolve, reject) { var r = new FileReader(); r.onload = function () { resolve(r.result); }; r.onerror = reject; r.readAsText(f); });
2309
+ readPromise.then(function (content) {
2310
+ try { e.target.value = ""; } catch (err) {}
2311
+ api({ action: "import", content: String(content), filename: f.name }).then(function (res) {
2312
+ if (res && res.ok) {
2313
+ setImportMsg({ seq: [{ k: "导入成功:新增 " }, res.imported, { k: " 条,跳过重复 " }, res.skipped, { k: " 条,忽略无效 " }, res.invalid, { k: " 条,现有共 " }, res.total, { k: " 条" }] });
2314
+ refresh();
2315
+ } else {
2316
+ setImportMsg({ k: "导入失败:", tail: (res && res.error) || t("未知错误") });
2317
+ }
2318
+ }).catch(function (err) { setImportMsg({ k: "导入失败:", tail: String((err && err.message) || err) }); });
2319
+ }).catch(function () { setImportMsg({ k: "读取文件失败" }); });
2320
+ }
2321
+
2322
+ var records = data.records || [];
2323
+ var interruptedTotal = 0;
2324
+ for (var ixi = 0; ixi < records.length; ixi++) { if (records[ixi].interrupted) interruptedTotal += 1; }
2325
+ var dataPath = data.dataPath || "";
2326
+ var nowPeriod = periodNow(Date.now());
2327
+ var effectiveAt = data.effectiveAt || 0;
2328
+ var nowEffective = effectiveAt ? (Date.now() >= effectiveAt) : true;
2329
+ // dsh-usage-scope:容器查询的锚点,让表格按"面板宽度"而不是"窗口宽度"折叠
2330
+ return el("div", { className: "dsh-usage-scope", style: st.root },
2331
+ el("canvas", { ref: function (n) { canvasNode = n; }, style: { display: "none" } }),
2332
+ el("div", { style: st.head },
2333
+ el("div", { style: st.headleft },
2334
+ el("div", null,
2335
+ el("div", { style: st.title },
2336
+ t("用量与消耗"),
2337
+ // 版本号直接来自宿主读到的 package.json(data.version),不在前端写死,
2338
+ // 避免"面板显示旧版本号、实际跑新版本"这种排查事故。
2339
+ el("span", { style: st.titleVer, title: t("当前运行的插件版本") }, "v" + (data.version || "?"))
2340
+ ),
2341
+ el("div", { style: st.sub }, t("记录插件激活后的每一次模型调用"))
2342
+ ),
2343
+ el("span", { style: nowPeriod.peak ? st.badgePeak : st.badgeValley }, t("当前 · ") + t(nowPeriod.label)),
2344
+ el("span", { style: nowEffective ? st.badgeHit : st.badgeValley }, nowEffective ? t("新价格已生效") : t("新价格未生效"))
2345
+ ),
2346
+ el("div", { style: st.actions },
2347
+ el("button", { style: st.btn, onClick: refresh }, t("刷新")),
2348
+ el("button", { style: st.btn, onClick: doClear }, t("清空"))
2349
+ )
2350
+ ),
2351
+ el("div", { style: st.subtabBar },
2352
+ SUBTABS.map(function (sub) {
2353
+ return el("button", {
2354
+ key: sub.k,
2355
+ style: tab === sub.k ? st.subtabOn : st.subtab,
2356
+ onClick: function () { setTab(sub.k); }
2357
+ }, t(sub.z));
2358
+ })
2359
+ ),
2360
+ // 工具条:说明只在需要时展开(导出/导入已归到「设置」页签)
2361
+ el("div", { style: st.calBar, marginTop: 8 },
2362
+ el("button", { style: infoOpen ? st.btnPrimary : st.btn, onClick: function () { setInfoOpen(!infoOpen); } },
2363
+ (infoOpen ? t("收起说明") : t("口径与版本")) + (interruptedTotal > 0 ? " · " + fmtInt(interruptedTotal) + t(" 次中断") : ""))
2364
+ ),
2365
+ infoOpen
2366
+ ? el("div", { style: st.diffbox },
2367
+ el("div", { style: st.diffboxTitle }, t("本地统计与官方后台的差异")),
2368
+ el("div", { style: st.diffboxText }, t("本面板统计的是插件在本地捕获的模型调用(按官方价格与峰谷时段计费)。与官方后台(platform.deepseek.com 用量页)相比,金额可能更低,常见原因:")),
2369
+ el("div", { style: st.diffboxText }, t("① 中断/出错/超时的调用:官方仍按实际 token 计费,插件无法获取其用量,按 0 记录(明细中标「中断」);")),
2370
+ el("div", { style: st.diffboxText }, t("② 账号下其他 API Key(如其它应用/脚本)的调用不经过 DeepSeek Harness,官方统计包含它们,本插件不包含;")),
2371
+ el("div", { style: st.diffboxText }, t("③ 如需精确对账,可在官方用量页导出月度账单 CSV 与本插件对比。")),
2372
+ interruptedTotal > 0
2373
+ ? el("div", { style: st.diffboxWarn }, t("当前记录中有 ") + fmtInt(interruptedTotal) + t(" 次中断调用(未计费)——对应官方后台已计费的部分。"))
2374
+ : null,
2375
+ // 价格分段 / 计价规则统一在「价格表」页的「价格来源与口径」里讲一次,
2376
+ // 这里不再重复(原先本抽屉、价格页、概览各有一份同样的说明文字)。
2377
+ el("div", { style: st.diffboxText, marginTop: 6 },
2378
+ t("计价规则与官方价格依据见「价格表」页;费用以 DeepSeek 官方后台账单为准。")),
2379
+ // 反馈问题时最先要确认的两条信息:版本号 + 数据目录。
2380
+ el("div", { style: st.diffboxText, marginTop: 6 },
2381
+ t("插件版本 ") + "v" + (data.version || "?") + t(" · 数据目录 ") + (dataPath || "—"))
2382
+ )
2383
+ : null,
2384
+ loading
2385
+ ? el(LoadingView, null)
2386
+ : el("div", null,
2387
+ tab === "overview" ? el(OverviewView, { records: records, regime: "auto", toolCalls: data.toolCalls || [], days: data.days || [], budget: data.budget, legacyPricingCount: data.legacyPricingCount || 0 }) : null,
2388
+ tab === "calendar" ? el(CalendarView, { records: records, regime: "auto", days: data.days || [] }) : null,
2389
+ tab === "detail" ? el("div", null,
2390
+ el(BreakdownView, { records: records, regime: "auto" }),
2391
+ el("div", { style: Object.assign({}, st.sec, { marginTop: 16 }) }, t("缓存命中明细")),
2392
+ el(CacheListView, { records: records, regime: "auto" })
2393
+ ) : null,
2394
+ tab === "settings" ? el("div", null,
2395
+ el(PriceView, { pricingHistory: data.pricingHistory || [], pricingMeta: data.pricingMeta || null, pricing: data.pricing, effectiveAt: data.effectiveAt, effectiveV41At: data.effectiveV41At, fx: data.fx || {}, budget: data.budget, legacyPricingCount: data.legacyPricingCount || 0, onBudgetChange: saveBudget, onFxRefresh: function () { api({ action: "fxRefresh" }).then(refresh).catch(function () {}); }, onRepriceAll: function () { return api({ action: "repriceAll" }).then(refresh).catch(function () {}); }, onChanged: refresh }),
2396
+ // 导出 / 导入 / 目标目录 / 数据路径:属于"配置与运维",统一落在设置页,
2397
+ // 不再在每个子页签底部常驻。
2398
+ el("div", { style: Object.assign({}, st.diffbox, { marginTop: 12 }) },
2399
+ el("div", { style: st.diffboxTitle }, t("导出与数据")),
2400
+ el("div", { style: st.actions, marginTop: 8 },
2401
+ el("button", { style: st.btn, onClick: function () { doExport("csv"); } }, t("导出 CSV")),
2402
+ el("button", { style: st.btn, onClick: function () { doExport("json"); } }, t("导出 JSON")),
2403
+ el("button", { style: st.btn, onClick: doExportImage }, t("导出图片 (PNG)")),
2404
+ el("button", { style: st.btn, onClick: doReveal }, t("打开目录")),
2405
+ el("button", { style: st.btn, onClick: pickFile }, t("选择文件导入"))
2406
+ ),
2407
+ exportMsg ? el("div", { style: st.note, marginTop: 6 }, __msgText(exportMsg)) : null,
2408
+ importMsg ? el("div", { style: st.note, marginTop: 6 }, __msgText(importMsg)) : null,
2409
+ el("div", { style: st.actions, marginTop: 8 },
2410
+ el("input", { style: st.input, placeholder: t("导出目标目录(留空 = 默认数据目录)"), value: destDir, onChange: function (e) { setDestDir(e.target.value); } }),
2411
+ el("button", { style: st.btn, onClick: pickDestDir }, t("选择目录…")),
2412
+ destDir ? el("button", { style: st.btn, onClick: function () { setDestDir(""); setExportMsg({ k: "已恢复默认数据目录" }); } }, t("重置")) : null
2413
+ ),
2414
+ dataPath
2415
+ ? el("div", { style: st.note, marginTop: 8 }, t("数据持久化:") + dataPath + t("(每次调用实时落盘,插件重启后自动恢复,最多保留 100000 条)"))
2416
+ : el("div", { style: st.note, color: C.danger, opacity: 1, marginTop: 8 }, t("持久化未启用:") + (data.persistError || t("未知原因")))
2417
+ )
2418
+ ) : null
2419
+ ),
2420
+ error ? el("div", { style: st.err }, error) : null,
2421
+ // 持久化失败必须始终可见(导出/导入已归到「设置」页签)。
2422
+ dataPath ? null : el("div", { style: st.note, color: C.danger, opacity: 1 }, t("持久化未启用:") + (data.persistError || t("未知原因")))
2423
+ );
2424
+ }
2425
+
2426
+ // ── plugin ──
2427
+ var inject = ["slots"];
2428
+
2429
+ function apply(ctx) {
2430
+ var slots = ctx.get("slots");
2431
+ if (slots === undefined) return;
2432
+ var timer = ctx.get("timer");
2433
+ // 语言跟随宿主设置(通用设置 → 语言):读取当前语言并订阅变化,切换后即时生效
2434
+ var locale = ctx.get("locale");
2435
+ if (locale) syncWithSystemLocale(locale);
2436
+ if (ctx.on) ctx.on("locale/change", function (snap) { setLang(snap && snap.active === "zh" ? "zh" : "en"); });
2437
+
2438
+ slots.inject("conversation.view", function () {
2439
+ return slots.register(
2440
+ { name: "conversation.view", id: "usage-cost-view", order: 20, label: function () { return t("用量与消耗"); } },
2441
+ function () { return el("div", { style: st.tab }, el(UsagePanel, { timer: timer })); }
2442
+ );
2443
+ });
2444
+
2445
+ slots.inject("settings.section", function () {
2446
+ return slots.register(
2447
+ { name: "settings.section", id: "usage-cost", order: 30, label: function () { return t("用量与消耗"); } },
2448
+ function () { return el(UsagePanel, { timer: timer }); }
2449
+ );
2450
+ });
2451
+
2452
+ }
2453
+
2454
+ // test/debug surface: instant-switch guarantees, locale formatters, peak/off-peak
2455
+ // judgment, and the calendar grid layout (which column the 1st falls into).
2456
+ // (此前这里把同一个对象赋值了两遍,属于重复代码,一并去掉。)
2457
+ exports.__i18n = {
2458
+ t: t,
2459
+ msgText: __msgText,
2460
+ setLang: setLang,
2461
+ dayLabel: dayLabel,
2462
+ fmtMonthLabel: fmtMonthLabel,
2463
+ dailyStatsTitle: dailyStatsTitle,
2464
+ weekdayLabels: weekdayLabels,
2465
+ isPeakNow: isPeakNow,
2466
+ periodNow: periodNow,
2467
+ monthOffset: monthOffset,
2468
+ monthDays: monthDays
2469
+ };
2470
+ // test/debug surface: 视图组件本身,供渲染冒烟测试直接调用
2471
+ // (只做"能否渲染出树"的检查,不涉及宿主 slot 注册)。
2472
+ exports.__views = {
2473
+ OverviewView: OverviewView,
2474
+ CalendarView: CalendarView,
2475
+ PriceView: PriceView,
2476
+ CacheListView: CacheListView,
2477
+ BreakdownView: BreakdownView
2478
+ };
2479
+ // test/debug surface: 图表刻度排布(纯函数,便于直接验证不重叠)
2480
+ exports.__charts = { trendDateTicks: trendDateTicks };
2481
+ exports.inject = inject;
2482
+ exports.apply = apply;
2483
+ return module.exports;
2484
+ }
2485
+ });