@feiyang666/dsh-usage-plugin 1.13.1 → 1.14.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 CHANGED
@@ -6,12 +6,25 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  var React = require("react");
8
8
  var el = React.createElement;
9
+ // react-dom 是平台种子模块:用于把弹窗 Portal 到 document.body,
10
+ // 避免对话树祖先的 transform/contain 破坏 position:fixed(弹窗不可见)。
11
+ var ReactDOM = null;
12
+ try { ReactDOM = require("react-dom"); } catch (e2) {}
13
+ // 隐藏弹窗卡片滚动条外观(仍可滚轮滚动),并在小屏下贴近视口
14
+ try {
15
+ if (typeof document !== "undefined" && document.head && !document.getElementById("dsh-usage-tok-style")) {
16
+ var __tokStyle = document.createElement("style");
17
+ __tokStyle.id = "dsh-usage-tok-style";
18
+ __tokStyle.textContent = "[data-dsh-usage-tok]{scrollbar-width:none;-ms-overflow-style:none}[data-dsh-usage-tok]::-webkit-scrollbar{width:0;height:0;display:none}";
19
+ document.head.appendChild(__tokStyle);
20
+ }
21
+ } catch (e3) {}
9
22
 
10
23
  // ── i18n: zh keys → en dictionary (community addition) ──
11
- var __T_EN = {"未知模型":"Unknown model","未知服务商":"Unknown provider","完成":"Done","工具调用":"Tool call","超长":"Overlong","错误":"Error","自动(生效前基础价 · 生效后峰谷价)":"Auto (base price before effective date · peak/valley after)","基础价格":"Base price","峰谷价格":"Peak/valley price","自动":"Auto","canvas 2d 不可用":"canvas 2D unavailable","用量与消耗报告":"Usage & Cost Report","生成时间 ":"Generated ","(北京) · 共 ":" (Beijing) · total "," 条调用 · 缓存命中率 ":" calls · cache-hit rate "," · 高峰价消耗 ":" · peak-rate cost "," · 空闲价消耗 ":" · off-peak cost "," · 合计 ":" · total "," · 报告仅含最近 ":" · report limited to the latest "," 条":" records","调用次数":"Calls","输入 · 未命中":"Input · miss","输入 · 缓存命中":"Input · cache hit","输出":"Output","高峰消耗":"Peak cost","空闲消耗":"Off-peak cost","总消耗 (峰谷价)":"Total cost (peak/valley)","缓存命中列表(共 ":"Cache hit list ("," 条)":" records)","时间(北京)":"Time (Beijing)","模型":"Model","输入·未命中":"Input·miss","缓存命中":"Cache hit","缓存写入":"Cache write","推理":"Reasoning","命中率":"Hit rate","时段":"Period","结束":"End","消耗(峰谷)":"Cost (peak/valley)","峰":"Peak","谷":"Valley","消耗表(按模型 · 峰谷价 · 高峰/空闲分列)":"Cost table (by model · peak/valley prices)","调用":"Calls","总消耗":"Total cost","合计":"Total","消耗表(按 API 服务商 × 模型 · 峰谷价)":"Cost table (by API provider × model · peak/valley)","API 服务商 / 模型":"API provider / model"," 个模型)":" models)","总费用合计":"Total cost overall","计价按请求的 API 服务商分别应用已核验价格;DigitalOcean 美元价按 USD/CNY 汇率折算;无法可靠映射的第三方价格按 ¥0。":"Billing applies verified prices per requesting API provider; DigitalOcean USD prices are converted at the USD/CNY rate; third-party prices that cannot be mapped reliably are counted as ¥0."," 个服务商":" providers","今天":"Today","近7天":"Last 7 days","近30天":"Last 30 days","全部":"All","自定义区间:":"Custom range:","至":"to","清除筛选":"Clear filters","当前范围:共 ":"Current range: ","(全部记录)":"(all records)"," · 总消耗 ":" · total cost ","高峰 ":"Peak "," · 空闲 ":" · off-peak ","命中率 ":"hit rate ","高峰时段 9:00–12:00、14:00–18:00":"Peak hours 9:00–12:00 & 14:00–18:00","其余空闲时段":"other hours are off-peak","高峰 + 空闲 · ":"Peak + off-peak · ","消耗表(按模型)":"Cost table (by model)","API 服务商":"API provider","高峰/空闲":"Peak/Off-peak","消耗明细(按 API 服务商 × 模型)":"Cost breakdown (by API provider × model)","DeepSeek 官方请求按官方峰谷价;SiliconFlow 按自身人民币公开价;DigitalOcean 按美元公开价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。模型名与 API 服务商均以请求参数为准。":"DeepSeek official requests use official peak/valley prices; SiliconFlow uses its own public CNY prices; DigitalOcean uses public USD prices converted to CNY at the USD/CNY rate; Qwen is not billed yet; AMD GPU Cloud DeepSeek V4 Flash is free at ¥0. Model names and API providers follow the actual request parameters."," 次(高峰 ":" calls (peak "," / 空闲 ":" / off-peak "," · 缓存命中 ":" · cache hit "," · 输出 ":" · output "," · 推理 ":" · reasoning "," · 空闲消耗 ":" · off-peak cost ","‹ 上月":"‹ Prev month","下月 ›":"Next month ›","按消耗":"By cost","按调用数":"By calls","热力:":"Heatmap: ","低 → 高(":"low → high (","当日消耗":"Day cost","当日调用数":"Day calls","· 悬停查看详情,点击某天查看当日调用":"· hover for details, click a day for that day's calls","本月调用":"Calls this month","本月消耗":"Cost this month","本月暂无记录。":"No records this month.","日期":"Date","总消耗(自动)":"Total cost (auto)"," 调用明细":" call details"," 条 · ":" records · "," | 空闲 ":" | off-peak "," | 合计 ":" | total ","该日无记录。":"No records for this day.","消耗":"Cost","清除区间":"Clear range"," 条 · 输入·未命中 ":" records · input·miss "," · 缓存写入 ":" · cache write "," · 高峰消耗 ":" · peak cost ","该时间范围内没有记录。":"No records in this range.","‹ 上一页":"‹ Prev page","第 ":"Page "," 页 · 每页 ":" page · per page "," 条 · 共 ":" records · total ","下一页 ›":"Next page ›","新价格表生效时间未知":"New price schedule effective date unknown","新价格表(峰谷价)将于 ":"The new peak/valley price schedule takes effect ","(北京时间)生效,当前按旧价格表(基础价)计费":" (Beijing time); currently billing on the old base-price schedule","新价格表(峰谷价)已生效(自 ":"New peak/valley schedule in effect (since "," 起)":")","今日 $ / ¥ 汇率":"Today's $/¥ rate","暂不可用":"Temporarily unavailable","汇率日期 ":"Rate date ","等待获取":"Pending fetch","汇率状态":"Rate status","缓存汇率":"Cached rate","最新可用":"Latest available","获取失败":"Fetch failed","刷新汇率":"Refresh rate","美元计价的 DigitalOcean 消耗按该 USD/CNY 汇率换算成人民币;人民币定价不受汇率影响。":"USD-denominated DigitalOcean costs are converted to CNY at this rate; CNY pricing is unaffected by it.","DeepSeek 官方 API 价格表":"DeepSeek Official API Price Table","本表仅用于 DeepSeek 官方 Provider;第三方 Provider 按下方已核验的自身价格计费,无法可靠匹配价格的模型按 0 统计。价格按官方公布固定,不可编辑。":"This table applies only to the official DeepSeek provider; third-party providers bill at their own verified prices below, and models without a reliable price match are counted as 0. Prices are fixed as officially published and are not editable.","峰谷价":"Peak/valley","基础价":"Base","自动模式:按调用时间自动选择计费档位——新价格表生效前的调用按基础价(旧价格表),生效后的调用按峰谷价(高峰时段 9:00–12:00、14:00–18:00 用高峰价,其余空闲时段用空闲价)。当前显示:":"Auto mode: the billing tier is chosen automatically by call time — calls made before the new schedule took effect use base (old) prices, calls after it use peak/valley prices (peak hours 9:00–12:00 & 14:00–18:00 at peak price, other hours at off-peak). Currently shown:","基础价表(旧价格表,新价格生效前)":"Base price table (old schedule, before new prices take effect)","峰谷价表(新价格表)":"Peak/valley table (new schedule)","。价格按官方公布固定,不可编辑。":". Prices are fixed as officially published and are not editable.","缓存命中(输入)":"Cache hit (input)","空闲 · 缓存命中":"Off-peak · cache hit","空闲 · 输入":"Off-peak · input","空闲 · 输出":"Off-peak · output","高峰 · 缓存命中":"Peak · cache hit","高峰 · 输入":"Peak · input","高峰 · 输出":"Peak · output","单位:元 / 百万 tokens。高峰时段(北京时间 9:00–12:00、14:00–18:00)用高峰价,其余空闲时段用空闲价(空闲价 = 高峰价的一半)。概览、用量日历、缓存命中列表中的消耗均已按高峰 / 空闲分列统计。":"Unit: CNY per million tokens. Peak hours (Beijing 9:00–12:00 & 14:00–18:00) use peak prices; other hours use off-peak (= half of peak). Costs in Overview, Usage Calendar and Cache Hit List are split into peak / off-peak accordingly.","第三方平台价格与覆盖状态(2026-08-19 核验)":"Third-party platform prices & coverage (verified 2026-08-19)","只把能与记录中的 provider/model 可靠匹配的价格用于自动计费;美元价格先按 USD 计算,再乘 USD/CNY 汇率统一折算成人民币。":"Only prices that reliably match a record's provider/model are used for automatic billing; USD prices are computed in USD first, then converted to CNY at the USD/CNY rate.","模型 / 状态":"Model / status","输入":"Input","说明":"Notes","已用于自动计费":"Used for auto billing","缓存命中价按 2026-08-03 起生效公告":"Cache-hit price per the notice effective 2026-08-03","按输入价":"Billed at input price","公告未单列缓存命中价":"The notice lists no separate cache-hit price","自动按 USD/CNY 汇率换算人民币并计费":"Auto-converted to CNY at the USD/CNY rate and billed","Alibaba / 千问":"Alibaba / Qwen","暂不计费":"Not billed yet","调用与 token 正常统计;费用暂按 ¥0":"Calls and tokens tracked normally; cost counted as ¥0 for now","免费;费用固定按 ¥0":"Free; cost fixed at ¥0","DigitalOcean 使用美元官方价并按最新可用 USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud 的 DeepSeek V4 Flash 免费按 ¥0。":"DigitalOcean bills at official USD prices converted via the latest available USD/CNY rate; Qwen is not billed yet; AMD GPU Cloud DeepSeek V4 Flash is free at ¥0.","概览":"Overview","用量日历":"Usage Calendar","缓存命中列表":"Cache Hit List","价格表":"Price Table","已导出:":"Exported: ","导出失败:":"Export failed: ","未知错误":"unknown error","导出中…":"Exporting…","生成图片中…":"Rendering image…","画布不可用":"Canvas unavailable","生成图片失败:":"Image rendering failed: ","打开目录选择…":"Open directory picker…","导出目标:":"Export destination:","选择目录失败:":"Failed to pick directory: ","打开文件夹失败:":"Failed to open folder: ","读取文件…":"Reading file…","导入成功:新增 ":"Import OK: added "," 条,跳过重复 ":", skipped duplicates "," 条,忽略无效 ":", ignored invalid "," 条,现有共 ":", total now ","导入失败:":"Import failed: ","读取文件失败":"Failed to read file","用量与消耗":"Usage & Cost","记录插件激活后的每一次模型调用":"Logs every model call since the plugin was activated","当前 · 高峰时段":"Current · peak hours","当前 · 空闲时段":"Current · off-peak hours","新价格已生效":"New prices in effect","新价格未生效":"New prices not yet in effect","刷新":"Refresh","清空":"Clear","计价说明:DeepSeek 官方与 SiliconFlow 按人民币价格;DigitalOcean 按美元官方价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。":"Billing notes: DeepSeek official and SiliconFlow bill in CNY; DigitalOcean bills at official USD prices × USD/CNY rate; Qwen is not billed yet; AMD GPU Cloud DeepSeek V4 Flash is free at ¥0.","导出 CSV":"Export CSV","导出 JSON":"Export JSON","导出图片 (PNG)":"Export image (PNG)","打开目录":"Open folder","导出目标目录(留空 = 默认数据目录)":"Export target directory (empty = default data dir)","选择目录…":"Choose directory…","已恢复默认数据目录":"Restored default data directory","重置":"Reset","选择文件导入":"Choose a file to import","数据持久化:":"Data persistence:","(每次调用实时落盘,插件重启后自动恢复,最多保留 100000 条)":"(every call is written to disk live, restored automatically after restart, up to 100000 records)","持久化未启用:":"Persistence disabled:","未知原因":"unknown reason","DEEPSEEK_API_KEY(推理 Key)":"DEEPSEEK_API_KEY (inference key)","模型设置中 Provider ID 或显示名为 siliconflow 的提供商 API Key":"API key of the provider whose Provider ID or display name is siliconflow in model settings","打开 SiliconFlow API 密钥":"Open SiliconFlow API keys","账户级 dop_v1_ Personal Access Token;Read Only(api:read)或 billing:read":"Account-level dop_v1_ Personal Access Token; Read Only (api:read) or billing:read","创建 DigitalOcean Account API Token":"Create a DigitalOcean account API token","暂无公开余额 API,仅支持控制台查看":"No public balance API yet — console only","打开 AMD Developer Cloud":"Open AMD Developer Cloud","查询失败":"Query failed","无法读取凭据状态":"Cannot read credential status","请输入以 dop_v1_ 开头的 DigitalOcean 账户 PAT。":"Enter a DigitalOcean account PAT starting with dop_v1_.","保存失败":"Save failed","使用凭据 ":"Using credential ","来源 ":"source ","模型提供商 ":"Model provider ","端点 ":"Endpoint "," · 账户余额":" · account balance","USD · 预付款/信用余额":"USD · prepaid/credit balance","USD · 待结算金额":"USD · pending amount","USD · 当前无余额":"USD · no balance"," · 账户可用":" · account available"," · 账户不可用":" · account unavailable"," · 查询成功":" · query succeeded","剩余余额查询":"Remaining balance query","查询 DeepSeek、SiliconFlow 与 DigitalOcean 账户余额":"Query DeepSeek, SiliconFlow and DigitalOcean account balances","打开控制台":"Open console","查询中…":"Querying…","查询账单":"Query billing","查询余额":"Query balance","所需凭据:":"Required credentials:","仅支持控制台查看":"Console only","插件不会尝试未经公开文档确认的端点,也不会把 AMD 推理 Key 当作账单凭据。":"The plugin never probes endpoints unconfirmed by public docs, and never uses AMD inference keys as billing credentials.","请确认凭据类型正确、权限包含余额/账单读取,且网络可访问服务商官方 API。":"Please verify the credential type is correct, its permissions include balance/billing read, and the provider's official API is reachable from this network.","打开服务商凭据页面":"Open provider credentials page","正在查询余额…":"Querying balance…","可用":"Available","不可用":"Unavailable","账单更新时间":"Billing updated","查询时间":"Queried at","北京时间":"Beijing time","请创建账户级 Personal Access Token。可选 Read Only(api:read,包含 billing:read),或自定义 billing:read;不要使用 Gradient AI 推理 Key。":"Please create an account-level Personal Access Token. Choose Read Only (api:read incl. billing:read) or a custom scope with billing:read; do not use a Gradient AI inference key.","正在检查已保存的 Token…":"Checking saved token…","已保存:":"Saved: ","(来源:":" (source: ","无法读取 Token 状态:":"Cannot read token status: ","尚未保存 DIGITALOCEAN_TOKEN。":"DIGITALOCEAN_TOKEN is not saved yet.","输入新的 dop_v1_ Token 可替换(当前值已隐藏)":"Enter a new dop_v1_ token to replace it (current value hidden)","保存中…":"Saving…","保存并查询":"Save & query","当前 Token 来自只读环境变量;请在原来源中修改,页面不会覆盖它。":"The current token comes from a read-only environment variable; edit it at its source — this panel will not overwrite it.","Token 已安全保存;页面和 API 响应不会回传明文。":"Token stored securely; neither the page nor API responses echo it back.","SiliconFlow 查询说明":"SiliconFlow query notes","公开 API 已成功返回 ¥0.00。SiliconFlow 的 /v1/user/info 当前不返回代金券或历史用量;控制台可用总额可能非零。这里忠实展示 API 原始余额字段,不把它等同于控制台完整额度。":"The public API returned ¥0.00 successfully. SiliconFlow's /v1/user/info currently returns no vouchers or historical usage; the console total may still be non-zero. The raw balance field from the API is shown faithfully here and should not be read as the full console credit.","余额卡片展示 SiliconFlow /v1/user/info 公开 API 返回的原始余额字段。":"The balance card shows the raw balance field returned by SiliconFlow's public /v1/user/info API.","返回字段":"Returned fields","凭据读取规则":"Credential lookup rules","插件只检查“设置 → 模型”中 Provider ID 或显示名为 siliconflow 的提供商,并读取其 apiKeyEnv 对应的已保存 API Key。":"The plugin only checks providers in Settings → Models whose Provider ID or display name is siliconflow, and reads the saved API key referenced by their apiKeyEnv.","如果未找到提供商、未填写 API Key 或凭据引用失效,插件会停止查询并说明需要修复的配置;不会回退到其他服务商的 Key。":"If no such provider exists, the key is empty, or the credential reference is broken, the plugin stops querying and explains what needs fixing; it never falls back to another provider's key.","DigitalOcean 查询的是主账户 Billing API;DigitalOcean AI 推理 Key 与 AMD GPU Cloud 推理 Key 均不能直接查询账单。":"DigitalOcean queries the main account Billing API; DigitalOcean AI inference keys and AMD GPU Cloud inference keys cannot query billing directly.","\n调用 ":"\ncalls ",")\n输入·未命中 ":")\ninput·miss ","\n高峰消耗 ":"\npeak cost ","\n无记录":"\nno records","查询 DeepSeek、SiliconFlow、DigitalOcean 与百炼 Token Plan":"Query DeepSeek, SiliconFlow, DigitalOcean & Bailian Token Plan balances","百炼 Token Plan · 1 周配额":"Bailian Token Plan · 1-week quota","百炼 Token Plan 配额用量":"Bailian Token Plan quota usage","百炼 Token Plan":"Bailian Token Plan","本周配额":"This week's quota","开始":"Start","结束":"End","已用 ":"Used ","百炼 Token Plan 查询说明":"Bailian Token Plan query notes","本查询复用百炼 CLI(bl)的“控制台登录”OAuth token,不需要阿里云 AccessKey,权限面很小。":"This query reuses the console-login OAuth token from the Bailian CLI (bl); no Alibaba Cloud AccessKey is needed and the permission scope is minimal.","如尚未登录,请先在终端执行:bl auth login --console,然后回到本页点“查询余额”。":"If you haven't logged in yet, run: bl auth login --console, then come back and click 'Query balance'.","打开百炼 CLI 登录说明":"Open Bailian CLI login docs","百炼控制台 OAuth 登录(~/.bailian/config.json 的 access_token,用 bl auth login --console 生成)":"Bailian console OAuth login (access_token in ~/.bailian/config.json, created by bl auth login --console)","数据来自阿里云百炼控制台内部门户网关(zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage),展示的是 1 周配额已用进度与周期。":"Data from Alibaba Cloud Bailian console portal gateway (zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage), showing 1-week quota usage progress and period."};
24
+ var __T_EN = {"本期无消耗(所有用量为 0)":"No usage this turn (all zeros)","按模型":"By model","…及另外 ":"…plus "," 个模型(完整列表见「用量与消耗」页签)":" more models (full list in the Usage & Cost tab)","对话累计":"Conversation total","本轮明细":"This turn","本轮 token":"Turn tokens","本轮消耗":"Turn cost","本轮缓存命中率":"Turn hit rate","本轮耗时":"Turn duration","本轮无记录":"No records this turn","工具调用(内部)":"Tool calls (internal)"," 条内部调用":" internal calls","展开":"Expand","收起":"Collapse","详细记录请在「用量与消耗」页签查看":"Full details are in the Usage & Cost tab","获取中…":"Loading…","总 token":"Total tokens","Token 明细":"Token details","本次输出":"This output","总消耗":"Total cost","耗时":"Run time","缓存命中率":"Cache hit rate","关闭":"Close","无 Token 数据":"No token data","当前 · ":"Current · ","工作日高峰时段":"Weekday peak hours","工作日空闲时段":"Weekday off-peak hours","周末高峰时段":"Weekend peak hours","周末空闲时段":"Weekend off-peak hours","周一至周五 9:00–12:00、14:00–18:00":"Mon–Fri 9:00–12:00 & 14:00–18:00","工作日其余时间":"Other weekday hours","周末(周六、周日)全天按空闲价计费":"All weekend (Sat & Sun) billed at the off-peak rate","2026-08-23 前仍按原规则分峰谷":"Before 2026-08-23 the old peak/valley split still applies","高峰 / 空闲时段说明":"Peak / off-peak hours","工作日高峰":"Weekday peak","空闲时段":"Off-peak hours"," 周一至周五 9:00–12:00、14:00–18:00":" Mon–Fri 9:00–12:00 & 14:00–18:00"," 工作日其余时间,以及周末(周六、周日)全天(自 2026-08-23 起;此前仍按原规则分峰谷)":" Weekday off-peak hours, plus all weekend (Sat & Sun) since 2026-08-23 (before that, the old peak/valley split still applies)","工作日高峰 9:00–12:00、14:00–18:00 · 周末全天空闲价":"Weekday peak 9:00–12:00 & 14:00–18:00 · all-weekend off-peak rate","\n调用 ":"\ncalls ",")\n输入·未命中 ":")\ninput·miss ","\n高峰消耗 ":"\npeak cost ","\n无记录":"\nno records","自动模式:按调用时间自动选择计费档位——新价格表生效前的调用按基础价(旧价格表),生效后的调用按峰谷价(工作日高峰时段 9:00–12:00、14:00–18:00 用高峰价,其余时间——含周末全天——用空闲价)。当前显示:":"Auto mode: the billing tier is chosen automatically by call time — calls made before the new price schedule took effect are billed at base (old) prices; calls after it use peak/valley prices (weekday peak hours 9:00–12:00 & 14:00–18:00 at the peak price, all other times — including all weekend — at the off-peak price). Currently shown:","单位:元 / 百万 tokens。工作日高峰时段(北京时间 9:00–12:00、14:00–18:00)用高峰价,其余时间用空闲价(空闲价 = 高峰价的一半);自 2026-08-23 起周末(周六、周日)全天按空闲价计费,新规则生效前仍按原规则。概览、用量日历、缓存命中列表中的消耗均已按高峰 / 空闲分列统计。":"Unit: CNY per million tokens. Weekday peak hours (Beijing time 9:00–12:00 & 14:00–18:00) use the peak price; all other times use the off-peak price (off-peak = half of peak). Since 2026-08-23, weekends (Sat & Sun) are billed at the off-peak rate all day; calls before the new rule still follow the old split. Costs in Overview, Usage Calendar and the Cache Hit List are split into peak / off-peak accordingly.","未知模型":"Unknown model","未知服务商":"Unknown provider","完成":"Done","工具调用":"Tool call","超长":"Overlong","错误":"Error","已中断":"Interrupted","超时":"Timeout","中断":"Interrupted"," · 中断 ":" · interrupted ","(未计费)":" (not billed)","本地统计与官方后台的差异":"Local stats vs official console","本面板统计的是插件在本地捕获的模型调用(按官方价格与峰谷时段计费)。与官方后台(platform.deepseek.com 用量页)相比,金额可能更低,常见原因:":"This panel shows calls captured locally by the plugin (billed at official prices with peak/off-peak rates). Compared with the official console (platform.deepseek.com usage page), the amount may be lower — common reasons:","① 中断/出错/超时的调用:官方仍按实际 token 计费,插件无法获取其用量,按 0 记录(明细中标「中断」);":"① Interrupted/failed/timed-out calls: the console still bills their actual tokens, but the plugin cannot obtain their usage, so they are recorded as 0 (marked \"Interrupted\" in details);","② 账号下其他 API Key(如其它应用/脚本)的调用不经过 DeepSeek Harness,官方统计包含它们,本插件不包含;":"② Calls from other API keys on your account (e.g. other apps/scripts) do not pass through DeepSeek Harness — the console includes them, this plugin does not;","③ 如需精确对账,可在官方用量页导出月度账单 CSV 与本插件对比。":"③ For exact reconciliation, export the monthly billing CSV from the official usage page and compare it with this plugin.","当前记录中有 ":"Current records include "," 次中断调用(未计费)——对应官方后台已计费的部分。":" interrupted call(s) (not billed) — corresponding to the part already billed by the official console.","自动(生效前基础价 · 生效后峰谷价)":"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."};
12
25
 
13
26
  function __detectLang() {
14
- try { var v = window.localStorage.getItem("dsh-usage-lang"); if (v === "zh" || v === "en") return v; } catch (e) {}
27
+ // 语言完全跟随宿主设置(通用设置 语言):不再读 localStorage,避免旧选择覆盖系统语言
15
28
  try { var nav = (typeof navigator !== "undefined" && navigator.language) || "en"; return nav.toLowerCase().indexOf("zh") === 0 ? "zh" : "en"; } catch (e2) { return "en"; }
16
29
  }
17
30
  var __LANG = __detectLang();
@@ -101,10 +114,20 @@ window.__ModuleLoader__.load({
101
114
  var next = lang === "zh" ? "zh" : "en";
102
115
  if (next === __LANG) return __LANG;
103
116
  __LANG = next;
104
- try { window.localStorage.setItem("dsh-usage-lang", __LANG); } catch (e) {}
117
+ // 仅内存生效:不再写 localStorage,语言始终以宿主设置为准
105
118
  for (var i = 0; i < __langListeners.length; i++) { try { __langListeners[i](__LANG); } catch (e2) {} }
106
119
  return __LANG;
107
120
  };
121
+ // 跟随宿主语言服务(通用设置 → 语言):读取当前语言并订阅变化,切换后即时生效
122
+ var syncWithSystemLocale = function (locale) {
123
+ if (!locale || typeof locale.getLocale !== "function") return;
124
+ var sync = function () {
125
+ var snap = locale.getLocale();
126
+ setLang(snap && snap.active === "zh" ? "zh" : "en");
127
+ };
128
+ sync();
129
+ if (typeof locale.subscribe === "function") locale.subscribe(sync);
130
+ };
108
131
  function useLang() {
109
132
  var st = React.useState(__LANG);
110
133
  React.useEffect(function () {
@@ -146,11 +169,9 @@ window.__ModuleLoader__.load({
146
169
  subtab: { border: "1px solid rgba(128,128,128,.35)", background: "transparent", borderRadius: 6, padding: "5px 14px", fontSize: fs(12), cursor: "pointer", color: "inherit" },
147
170
  subtabOn: { border: "1px solid rgba(90,140,255,.5)", background: "rgba(90,140,255,.22)", borderRadius: 6, padding: "5px 14px", fontSize: fs(12), cursor: "pointer", color: "inherit", fontWeight: 600 },
148
171
  scroll: { overflowX: "auto", overflowY: "hidden", width: "100%", maxWidth: "100%", minWidth: 0 },
149
- // 表格宽度自适应容器(width:100% + 列内容允许换行),不再用 max-content 撑出横向滚动条;
150
- // 极窄窗口下容器仍保留 overflow-x 兜底。
151
- // minWidth:720 直接写在内联样式里(内联样式必然生效),不依赖下方注入的全局样式表:
152
- // 窄屏时表格保持 720px、外层 st.scroll 容器横向滚动,避免 9–11 列被压到逐字竖排。
153
- tbl: { width: "100%", minWidth: 720, borderCollapse: "collapse", fontSize: fs(12) },
172
+ // 表格按内容撑开(width:max-content),列不被压缩,数值不会换行/重叠;
173
+ // 外层 st.scroll(overflow-x:auto)在窗口不够宽时横向滚动。minWidth:720 兜底极窄屏。
174
+ tbl: { width: "max-content", minWidth: 720, borderCollapse: "collapse", fontSize: fs(12) },
154
175
  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 },
155
176
  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 },
156
177
  td: { textAlign: "right", padding: "7px 8px", borderBottom: "1px solid rgba(128,128,128,.2)", whiteSpace: "normal", wordBreak: "break-word" },
@@ -165,6 +186,7 @@ window.__ModuleLoader__.load({
165
186
  badgeHit: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(11), fontWeight: 500, background: "rgba(46,204,113,.18)", color: "#2ecc71", whiteSpace: "nowrap" },
166
187
  badgePeak: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(11), fontWeight: 500, background: "rgba(255,152,0,.18)", color: "#ff9800", whiteSpace: "nowrap" },
167
188
  badgeValley: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(11), fontWeight: 500, background: "rgba(90,140,255,.18)", color: "#5a8cff", whiteSpace: "nowrap" },
189
+ badgeWarn: { display: "inline-block", padding: "2px 8px", borderRadius: 999, fontSize: fs(11), fontWeight: 500, background: "rgba(231,76,60,.16)", color: "#e74c3c", whiteSpace: "nowrap" },
168
190
  costPeak: { color: "#e08700", fontWeight: 600, whiteSpace: "nowrap" },
169
191
  costOff: { color: "#3d6bd6", fontWeight: 600, whiteSpace: "nowrap" },
170
192
  err: { color: "#ff6b6b", fontSize: fs(12) },
@@ -172,6 +194,10 @@ window.__ModuleLoader__.load({
172
194
  note: { fontSize: fs(11), opacity: 0.55 },
173
195
  errbox: { border: "1px solid rgba(255,107,107,.4)", background: "rgba(255,107,107,.08)", borderRadius: 8, padding: 12, fontSize: fs(12) },
174
196
  errboxTitle: { fontWeight: 600, marginBottom: 4, color: "#ff6b6b" },
197
+ diffbox: { border: "1px solid rgba(255,152,0,.4)", background: "rgba(255,152,0,.07)", borderRadius: 8, padding: "8px 12px", marginTop: 10, fontSize: fs(11), lineHeight: 1.6, color: "inherit" },
198
+ diffboxTitle: { fontWeight: 600, marginBottom: 4, color: "#e08700" },
199
+ diffboxText: { opacity: 0.85, marginBottom: 2 },
200
+ diffboxWarn: { marginTop: 6, color: "#e74c3c", fontWeight: 600 },
175
201
  infobox: { border: "1px solid rgba(90,140,255,.35)", background: "rgba(90,140,255,.08)", borderRadius: 8, padding: 12, marginTop: 18, fontSize: fs(12) },
176
202
  infoboxTitle: { fontWeight: 600, marginBottom: 4, color: "#5a8cff" },
177
203
  actionLink: { display: "inline-block", marginTop: 8, color: "#3d6bd6", textDecoration: "underline", fontSize: fs(12) },
@@ -230,7 +256,10 @@ window.__ModuleLoader__.load({
230
256
  // 极窄屏 (≤ 540px):对带 collapse-mobile 的宽表隐藏第 4 列至倒数第 2 列,
231
257
  // 只保留前 3 个主标识列 + 最后合计列(如 消耗表/明细表 保留 模型/服务商/调用/总消耗),
232
258
  // 避免每列被压到极限宽度而逐字竖排。价格表等窄表不折叠,保留横向滚动。
233
- "@media (max-width:540px){.dsh-usage-table.collapse-mobile tr>*:not(:nth-child(-n+3)):not(:last-child){display:none}.dsh-usage-table.collapse-mobile{min-width:320px}}";
259
+ "@media (max-width:540px){.dsh-usage-table.collapse-mobile tr>*:not(:nth-child(-n+3)):not(:last-child){display:none}.dsh-usage-table.collapse-mobile{min-width:320px}}" +
260
+ // 移动端适配(≤900px):表格贴合屏宽、不出现横向滑条;宽表同样折叠中间列。
261
+ // !important 覆盖内联 minWidth 与其它插件的 min-width 规则。
262
+ "@media (max-width:900px){.dsh-usage-table{min-width:0!important;width:100%!important;max-width:100%!important}.dsh-usage-table.collapse-mobile tr>*:not(:nth-child(-n+3)):not(:last-child){display:none!important}.dsh-usage-table.collapse-mobile{min-width:0!important}}";
234
263
  document.head.appendChild(styleSheetEl);
235
264
  }
236
265
 
@@ -269,7 +298,7 @@ window.__ModuleLoader__.load({
269
298
  var s = String(p == null ? "" : p).trim();
270
299
  return s ? s.toLowerCase() : "unknown";
271
300
  };
272
- 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("错误"); return f || "—"; };
301
+ 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 || "—"; };
273
302
  var pct = function (a, b) { return b > 0 ? (a / b * 100).toFixed(1) + "%" : "—"; };
274
303
  // 周末统一按空闲价规则生效时间:北京时间 2026-08-23 00:00(周日)。此前(含周末)仍按原峰谷分段。
275
304
  var WEEKEND_FLAT_AT = Date.parse("2026-08-23T00:00:00+08:00");
@@ -384,12 +413,12 @@ var api = function (payload) {
384
413
  // ── 计费时段说明(区分工作日高峰 / 工作日空闲 / 周末全天空闲)──
385
414
  function BillingHint(props) {
386
415
  return el("div", { style: st.billHint },
387
- el("div", { style: st.billHintTitle }, "高峰 / 空闲时段说明"),
416
+ el("div", { style: st.billHintTitle }, t("高峰 / 空闲时段说明")),
388
417
  el("div", null,
389
- el("span", { style: st.badgePeak }, "工作日高峰"), " 周一至周五 9:00–12:00、14:00–18:00"
418
+ el("span", { style: st.badgePeak }, t("工作日高峰")), t(" 周一至周五 9:00–12:00、14:00–18:00")
390
419
  ),
391
420
  el("div", null,
392
- el("span", { style: st.badgeValley }, "空闲时段"), " 工作日其余时间,以及周末(周六、周日)全天(自 2026-08-23 起;此前仍按原规则分峰谷)"
421
+ el("span", { style: st.badgeValley }, t("空闲时段")), t(" 工作日其余时间,以及周末(周六、周日)全天(自 2026-08-23 起;此前仍按原规则分峰谷)")
393
422
  )
394
423
  );
395
424
  }
@@ -518,10 +547,12 @@ var api = function (payload) {
518
547
  for (var r3 = 0; r3 < count; r3++) {
519
548
  var rec = records[r3];
520
549
  var hit = rec.cacheReadTokens || 0, miss = rec.inputTokens || 0, cost = rec.peakValleyCost || 0;
550
+ var intR = !!rec.interrupted;
521
551
  ctx.fillStyle = r3 % 2 === 1 ? "#fafbfc" : "#ffffff"; ctx.fillRect(P, y, W - 2 * P, rowH);
522
552
  var ty = y + Math.round(rowH * 0.68);
523
553
  ctx.textAlign = "left"; ctx.fillStyle = "#1c2733"; font(fBase, "400");
524
554
  ctx.fillText(fmtTime(rec.time), c1.time, ty); ctx.fillText(modelName(rec), c1.model, ty);
555
+ if (intR) { ctx.fillStyle = "#e74c3c"; ctx.fillText(t("中断"), c1.model + ctx.measureText(modelName(rec)).width + 12, ty); ctx.fillStyle = "#1c2733"; }
525
556
  ctx.textAlign = "right";
526
557
  ctx.fillText(fmtInt(miss), c1.missR, ty);
527
558
  ctx.fillStyle = "#22a45d"; ctx.fillText(fmtInt(hit), c1.hitR, ty);
@@ -533,8 +564,8 @@ var api = function (payload) {
533
564
  ctx.fillText(rec.reasoningTokens ? fmtInt(rec.reasoningTokens) : "—", c1.reasonR, ty);
534
565
  ctx.fillStyle = "#55617a"; ctx.fillText(pct(hit, hit + miss), c1.rateR, ty);
535
566
  ctx.fillStyle = rec.peak ? "#e08700" : "#3d6bd6"; ctx.fillText(rec.peak ? t("峰") : t("谷"), c1.periodR, ty);
536
- ctx.fillStyle = "#55617a"; ctx.fillText(finishLabel(rec.finishReason), c1.endR, ty);
537
- ctx.fillStyle = rec.peak ? "#e08700" : "#3d6bd6"; ctx.fillText(fmtMoney(cost), c1.costR, ty);
567
+ ctx.fillStyle = intR ? "#e74c3c" : "#55617a"; ctx.fillText(finishLabel(rec.finishReason), c1.endR, ty);
568
+ ctx.fillStyle = rec.peak ? "#e08700" : "#3d6bd6"; ctx.fillText(intR ? "—" : fmtMoney(cost), c1.costR, ty);
538
569
  y += rowH;
539
570
  }
540
571
 
@@ -628,6 +659,9 @@ var api = function (payload) {
628
659
  // ── Overview view ──
629
660
  function OverviewView(props) {
630
661
  var allRecords = props.records, regime = props.regime;
662
+ var toolCalls = props.toolCalls || [];
663
+ var toolOpenState = React.useState(false);
664
+ var toolOpen = toolOpenState[0], setToolOpen = toolOpenState[1];
631
665
  var presetState = React.useState("all");
632
666
  var preset = presetState[0], setPreset = presetState[1];
633
667
  var fromState = React.useState("");
@@ -652,10 +686,12 @@ var api = function (payload) {
652
686
  records.push(fr);
653
687
  }
654
688
  var totalCalls = records.length;
689
+ var totalInterrupted = 0;
655
690
  var totalHit = 0, totalMiss = 0, totalWrite = 0, totalOut = 0, totalReason = 0, totalCost = 0, peakTotal = 0, offTotal = 0, peakCalls = 0, offCalls = 0;
656
691
  var byModel = {}, byProvider = {}, byProviderModel = {};
657
692
  for (var i = 0; i < records.length; i++) {
658
693
  var r = records[i];
694
+ if (r.interrupted) totalInterrupted += 1;
659
695
  totalHit += r.cacheReadTokens || 0; totalMiss += r.inputTokens || 0; totalWrite += r.cacheWriteTokens || 0;
660
696
  totalOut += r.outputTokens || 0; totalReason += r.reasoningTokens || 0;
661
697
  var cost = costOf(r, regime);
@@ -711,6 +747,18 @@ var api = function (payload) {
711
747
  models.sort(function (a, b) { return b.cost - a.cost || (a.modelKey < b.modelKey ? -1 : 1); });
712
748
  providerGroups.push({ provider: gprov, models: models });
713
749
  }
750
+ // 内部工具/占位调用(单独一个可折叠分组)
751
+ var toolAgg = {};
752
+ for (var ti = 0; ti < toolCalls.length; ti++) {
753
+ var tr = toolCalls[ti];
754
+ var tk = (tr.provider || '') + '|' + (tr.model || '');
755
+ var tg = toolAgg[tk];
756
+ if (!tg) { tg = toolAgg[tk] = { provider: tr.provider || '—', model: tr.model || '—', calls: 0, cost: 0 }; }
757
+ tg.calls += 1; tg.cost += tr.autoCost || 0;
758
+ }
759
+ var toolRows = [];
760
+ for (var tki in toolAgg) toolRows.push(toolAgg[tki]);
761
+ toolRows.sort(function (a, b) { return b.calls - a.calls || (a.provider < b.provider ? -1 : 1); });
714
762
  // 服务商 × 模型明细行(组头行 + 组内模型行)
715
763
  var pmRows = [];
716
764
  for (var gi = 0; gi < providerGroups.length; gi++) {
@@ -781,7 +829,7 @@ var api = function (payload) {
781
829
  t("当前范围:共 ") + fmtInt(records.length) + t(" 条") + (preset === "all" && !fromDate && !toDate ? t("(全部记录)") : "") + t(" · 总消耗 ") + fmtMoney(totalCost) + "(" + costHint(regime) + ")"
782
830
  ),
783
831
  el("div", { style: st.cards },
784
- el(Card, { label: t("调用次数"), value: fmtInt(totalCalls), hint: t("高峰 ") + fmtInt(peakCalls) + t(" · 空闲 ") + fmtInt(offCalls) }),
832
+ el(Card, { label: t("调用次数"), value: fmtInt(totalCalls), hint: t("高峰 ") + fmtInt(peakCalls) + t(" · 空闲 ") + fmtInt(offCalls) + (totalInterrupted > 0 ? t(" · 中断 ") + fmtInt(totalInterrupted) + t("(未计费)") : "") }),
785
833
  el(Card, { label: t("输入 · 未命中"), value: fmtInt(totalMiss), hint: "token" }),
786
834
  el(Card, { label: t("输入 · 缓存命中"), value: fmtInt(totalHit), hint: t("命中率 ") + hitRate }),
787
835
  el(Card, { label: t("输出"), value: fmtInt(totalOut), hint: "token" }),
@@ -841,7 +889,29 @@ var api = function (payload) {
841
889
  )
842
890
  ),
843
891
  el("div", { style: st.note, marginTop: 8 },
844
- t("DeepSeek 官方请求按官方峰谷价;SiliconFlow 按自身人民币公开价;DigitalOcean 按美元公开价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。模型名与 API 服务商均以请求参数为准。"))
892
+ t("DeepSeek 官方请求按官方峰谷价;SiliconFlow 按自身人民币公开价;DigitalOcean 按美元公开价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。模型名与 API 服务商均以请求参数为准。")),
893
+ toolCalls.length ? el("div", { style: st.sec, marginTop: 14 },
894
+ el("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
895
+ el("span", { style: { fontSize: fs(13), fontWeight: 600 } }, t("工具调用(内部)") + " · " + fmtInt(toolCalls.length) + t(" 条内部调用")),
896
+ el("button", { style: st.btn, onClick: function () { setToolOpen(!toolOpen); } }, toolOpen ? t("收起") : t("展开"))
897
+ ),
898
+ toolOpen ? el("div", { style: st.scroll },
899
+ el("table", { className: "dsh-usage-table collapse-mobile", style: st.tbl },
900
+ el("thead", null, el("tr", null,
901
+ el("th", { style: st.thFirst }, t("API 服务商 / 模型")),
902
+ el("th", { style: st.th }, t("调用")),
903
+ el("th", { style: st.th }, t("总消耗"))
904
+ )),
905
+ el("tbody", null, toolRows.map(function (tg, idx) {
906
+ return el("tr", { key: String(idx) },
907
+ el("td", { style: st.tdFirst }, tg.model + " · " + tg.provider),
908
+ el("td", { style: st.td }, fmtInt(tg.calls)),
909
+ el("td", { style: st.td }, fmtMoney(tg.cost))
910
+ );
911
+ }))
912
+ )
913
+ ) : null
914
+ ) : null
845
915
  );
846
916
  }
847
917
 
@@ -1019,10 +1089,11 @@ var api = function (payload) {
1019
1089
  el("tbody", null, selRecords.map(function (rr) {
1020
1090
  var hit2 = rr.cacheReadTokens || 0, miss2 = rr.inputTokens || 0;
1021
1091
  var cost2 = costOf(rr, regime);
1092
+ var int2 = !!rr.interrupted;
1022
1093
  return el("tr", { key: rr.time },
1023
1094
  el("td", { style: st.tdFirst }, fmtTime(rr.time)),
1024
1095
  el("td", { style: st.tdWrap },
1025
- el("div", null, modelName(rr)),
1096
+ el("div", null, modelName(rr), int2 ? el("span", { style: st.badgeWarn, marginLeft: 6 }, t("中断")) : null),
1026
1097
  el("div", { style: st.sub }, providerName(rr.provider))
1027
1098
  ),
1028
1099
  el("td", { style: st.td }, fmtInt(miss2)),
@@ -1030,8 +1101,8 @@ var api = function (payload) {
1030
1101
  el("td", { style: st.td }, fmtInt(rr.outputTokens || 0)),
1031
1102
  el("td", { style: st.td }, rr.reasoningTokens ? fmtInt(rr.reasoningTokens) : "—"),
1032
1103
  el("td", { style: st.td }, rr.peak ? el("span", { style: st.badgePeak }, t("峰")) : el("span", { style: st.badgeValley }, t("谷"))),
1033
- el("td", { style: st.td }, finishLabel(rr.finishReason)),
1034
- el("td", { style: st.td }, el("span", { style: rr.peak ? st.costPeak : st.costOff }, fmtMoney(cost2)))
1104
+ el("td", { style: st.td }, int2 ? el("span", { style: st.badgeWarn }, finishLabel(rr.finishReason)) : finishLabel(rr.finishReason)),
1105
+ el("td", { style: st.td }, int2 ? el("span", { style: st.costOff }, "—") : el("span", { style: rr.peak ? st.costPeak : st.costOff }, fmtMoney(cost2)))
1035
1106
  );
1036
1107
  }))
1037
1108
  )
@@ -1132,10 +1203,11 @@ var api = function (payload) {
1132
1203
  pageRows.map(function (rr3, idx) {
1133
1204
  var hit3 = rr3.cacheReadTokens || 0, miss3 = rr3.inputTokens || 0;
1134
1205
  var cost3 = costOf(rr3, regime);
1206
+ var int3 = !!rr3.interrupted;
1135
1207
  return el("tr", { key: rr3.time + "-" + idx },
1136
1208
  el("td", { style: st.tdFirst }, fmtTime(rr3.time)),
1137
1209
  el("td", { style: st.tdWrap },
1138
- el("div", null, modelName(rr3)),
1210
+ el("div", null, modelName(rr3), int3 ? el("span", { style: st.badgeWarn, marginLeft: 6 }, t("中断")) : null),
1139
1211
  el("div", { style: st.sub }, (rr3.provider || "") + (rr3.purpose ? " · " + rr3.purpose : ""))
1140
1212
  ),
1141
1213
  el("td", { style: st.td }, fmtInt(miss3)),
@@ -1145,8 +1217,8 @@ var api = function (payload) {
1145
1217
  el("td", { style: st.td }, rr3.reasoningTokens ? fmtInt(rr3.reasoningTokens) : "—"),
1146
1218
  el("td", { style: st.td }, pct(hit3, hit3 + miss3)),
1147
1219
  el("td", { style: st.td }, rr3.peak ? el("span", { style: st.badgePeak }, t("峰")) : el("span", { style: st.badgeValley }, t("谷"))),
1148
- el("td", { style: st.td }, finishLabel(rr3.finishReason)),
1149
- el("td", { style: st.td }, el("span", { style: rr3.peak ? st.costPeak : st.costOff }, fmtMoney(cost3)))
1220
+ el("td", { style: st.td }, int3 ? el("span", { style: st.badgeWarn }, finishLabel(rr3.finishReason)) : finishLabel(rr3.finishReason)),
1221
+ el("td", { style: st.td }, int3 ? el("span", { style: st.costOff }, "—") : el("span", { style: rr3.peak ? st.costPeak : st.costOff }, fmtMoney(cost3)))
1150
1222
  );
1151
1223
  }),
1152
1224
  el("tr", null,
@@ -1310,7 +1382,7 @@ var api = function (payload) {
1310
1382
  ];
1311
1383
 
1312
1384
  function UsagePanel(props) {
1313
- var lang = useLang();
1385
+ var lang = useLang(); // 订阅语言变化:宿主切换语言时触发本面板重渲染(值本身仅作触发用)
1314
1386
  var state = React.useState({ records: [], count: 0, dataPath: "", persistOk: false, persistError: "", pricing: null, days: [] });
1315
1387
  var data = state[0], setData = state[1];
1316
1388
  var tabState = React.useState("overview");
@@ -1412,6 +1484,8 @@ var api = function (payload) {
1412
1484
  }
1413
1485
 
1414
1486
  var records = data.records || [];
1487
+ var interruptedTotal = 0;
1488
+ for (var ixi = 0; ixi < records.length; ixi++) { if (records[ixi].interrupted) interruptedTotal += 1; }
1415
1489
  var dataPath = data.dataPath || "";
1416
1490
  var nowPeriod = periodNow(Date.now());
1417
1491
  var effectiveAt = data.effectiveAt || 0;
@@ -1429,7 +1503,6 @@ var api = function (payload) {
1429
1503
  ),
1430
1504
  el("div", { style: st.actions },
1431
1505
  el("button", { style: st.btn, onClick: refresh }, t("刷新")),
1432
- el("button", { style: st.btn, title: "Language / 语言(外部标签页名在刷新页面后更新)", onClick: function () { setLang(lang === "en" ? "zh" : "en"); } }, lang === "en" ? "中文" : "EN"),
1433
1506
  el("button", { style: st.btn, onClick: doClear }, t("清空"))
1434
1507
  )
1435
1508
  ),
@@ -1442,6 +1515,16 @@ var api = function (payload) {
1442
1515
  }, t(sub.z));
1443
1516
  })
1444
1517
  ),
1518
+ el("div", { style: st.diffbox },
1519
+ el("div", { style: st.diffboxTitle }, t("本地统计与官方后台的差异")),
1520
+ el("div", { style: st.diffboxText }, t("本面板统计的是插件在本地捕获的模型调用(按官方价格与峰谷时段计费)。与官方后台(platform.deepseek.com 用量页)相比,金额可能更低,常见原因:")),
1521
+ el("div", { style: st.diffboxText }, t("① 中断/出错/超时的调用:官方仍按实际 token 计费,插件无法获取其用量,按 0 记录(明细中标「中断」);")),
1522
+ el("div", { style: st.diffboxText }, t("② 账号下其他 API Key(如其它应用/脚本)的调用不经过 DeepSeek Harness,官方统计包含它们,本插件不包含;")),
1523
+ el("div", { style: st.diffboxText }, t("③ 如需精确对账,可在官方用量页导出月度账单 CSV 与本插件对比。")),
1524
+ interruptedTotal > 0
1525
+ ? el("div", { style: st.diffboxWarn }, t("当前记录中有 ") + fmtInt(interruptedTotal) + t(" 次中断调用(未计费)——对应官方后台已计费的部分。"))
1526
+ : null
1527
+ ),
1445
1528
  el(BillingHint, null),
1446
1529
  el("div", { style: st.note, marginTop: 6 },
1447
1530
  t("计价说明:DeepSeek 官方与 SiliconFlow 按人民币价格;DigitalOcean 按美元官方价 × USD/CNY 汇率折算人民币;千问暂不计费;AMD GPU Cloud DeepSeek V4 Flash 免费按 ¥0。")
@@ -1449,7 +1532,7 @@ var api = function (payload) {
1449
1532
  loading
1450
1533
  ? el(LoadingView, null)
1451
1534
  : el("div", null,
1452
- tab === "overview" ? el(OverviewView, { records: records, regime: "auto" }) : null,
1535
+ tab === "overview" ? el(OverviewView, { records: records, regime: "auto", toolCalls: data.toolCalls || [] }) : null,
1453
1536
  tab === "calendar" ? el(CalendarView, { records: records, regime: "auto", days: data.days || [] }) : null,
1454
1537
  tab === "cache" ? el(CacheListView, { records: records, regime: "auto" }) : null,
1455
1538
  tab === "prices" ? el(PriceView, { pricing: data.pricing, effectiveAt: data.effectiveAt, fx: data.fx || {}, onFxRefresh: function () { api({ action: "fxRefresh" }).then(refresh).catch(function () {}); }, onChanged: refresh }) : null
@@ -1480,7 +1563,7 @@ var api = function (payload) {
1480
1563
 
1481
1564
  // ── Balance panel ──
1482
1565
  function BalancePanel() {
1483
- var lang = useLang();
1566
+ var lang = useLang(); // 订阅语言变化:宿主切换语言时触发本面板重渲染(值本身仅作触发用)
1484
1567
  var state = React.useState({ status: "idle", data: null, error: "" });
1485
1568
  var s = state[0], setS = state[1];
1486
1569
  var providerState = React.useState("deepseek");
@@ -1750,6 +1833,225 @@ var api = function (payload) {
1750
1833
  );
1751
1834
  }
1752
1835
 
1836
+ // ── 消息底部 token 入口(conversation.chat.assistant-actions)──
1837
+ // 时长格式化为「Xm Ys」/「X分Y秒」
1838
+ var fmtDur = function (ms) {
1839
+ if (!ms || ms <= 0) return "0s";
1840
+ var secs = Math.round(ms / 1000);
1841
+ var m = Math.floor(secs / 60), r = secs % 60;
1842
+ if (m > 0) return __LANG === "en" ? (m + "m" + r + "s") : (m + "分" + r + "秒");
1843
+ return __LANG === "en" ? (r + "s") : (r + "秒");
1844
+ };
1845
+
1846
+ var __tok = {
1847
+ overlay: { position: "fixed", inset: 0, zIndex: 2147483000, background: "rgba(0,0,0,.4)", display: "flex", alignItems: "center", justifyContent: "center", padding: 16 },
1848
+ card: { background: "#ffffff", color: "#1c2733", borderRadius: 12, maxWidth: "min(680px, 94vw)", width: "100%", maxHeight: "calc(100vh - 40px)", overflowY: "auto", scrollbarWidth: "none", msOverflowStyle: "none", boxShadow: "0 14px 44px rgba(0,0,0,.4)", padding: "14px 16px", fontSize: fs(12.5) },
1849
+ title: { display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: fs(14), fontWeight: 600, marginBottom: 10 },
1850
+ hint: { fontSize: fs(11.5), opacity: 0.6, marginBottom: 10 },
1851
+ row: { display: "flex", alignItems: "center", gap: 10, padding: "6px 0", borderBottom: "1px solid #eef1f5", fontSize: fs(12) },
1852
+ cellModel: { flex: "0 0 32%", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
1853
+ cellTime: { flex: "0 0 14%", opacity: 0.7, fontSize: fs(11) },
1854
+ cellT: { flex: "0 0 11%", textAlign: "right", whiteSpace: "nowrap" },
1855
+ cellPeriod: { flex: "0 0 6%", textAlign: "center", fontWeight: 600 },
1856
+ cellCost: { flex: "0 0 16%", textAlign: "right", whiteSpace: "nowrap", fontWeight: 600 },
1857
+ title2: { fontSize: fs(13), fontWeight: 600, margin: "12px 0 6px" },
1858
+ statRow: { display: "flex", flexWrap: "wrap", gap: 10, marginBottom: 12 },
1859
+ stat: { flex: "1 1 0", minWidth: 96, background: "#f5f7fa", borderRadius: 8, padding: "8px 10px" },
1860
+ statLabel: { fontSize: fs(11), opacity: 0.6, marginBottom: 2 },
1861
+ statValue: { fontSize: fs(15), fontWeight: 700 },
1862
+ barWrap: { display: "flex", alignItems: "center", gap: 10, marginBottom: 12 },
1863
+ barTrack: { flex: 1, height: 12, borderRadius: 999, background: "#eef1f5", overflow: "hidden" },
1864
+ barFill: { height: "100%", borderRadius: 999, background: "#22a45d", transition: "width .3s ease" },
1865
+ barLabel: { fontSize: fs(11.5), whiteSpace: "nowrap", opacity: 0.85 },
1866
+ mCard: { background: "#f7f8fa", borderRadius: 8, padding: "8px 10px", marginBottom: 8 },
1867
+ mHead: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8, marginBottom: 4 },
1868
+ mName: { fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontSize: fs(12), flex: "1 1 0", minWidth: 0 },
1869
+ mCost: { color: "#1c2733", fontWeight: 700, whiteSpace: "nowrap" },
1870
+ mLine: { fontSize: fs(11.5), opacity: 0.85, overflowWrap: "anywhere" },
1871
+ close: Object.assign({}, st.btn, { marginLeft: "auto" })
1872
+ };
1873
+
1874
+ function MessageTokenAction(props) {
1875
+ var messageId = props.messageId;
1876
+ var sessionId = props.sessionId;
1877
+ var openState = React.useState(false);
1878
+ var open = openState[0], setOpen = openState[1];
1879
+ var dataState = React.useState({ status: "idle", data: null });
1880
+ var s1 = dataState[0], setData = dataState[1];
1881
+ // 从会话快照读取该消息的完成时间与所在步骤开始时间,用作“本轮”时间窗
1882
+ var msgTime = 0, stepStart = 0;
1883
+ if (props.useSession && typeof props.useSession === "function") {
1884
+ try {
1885
+ var pick = function (f) {
1886
+ return props.useSession(function (snap) {
1887
+ if (!snap || !snap.chat || !snap.chat.nodes) return 0;
1888
+ var arr = typeof snap.chat.nodes.values === "function" ? snap.chat.nodes.values() : null;
1889
+ if (!arr) return 0;
1890
+ for (var ni = 0; ni < arr.length; ni++) {
1891
+ var vh = arr[ni];
1892
+ if (!vh || !vh.data) continue;
1893
+ var fin = vh.data.finalNode || (vh.data.closing && vh.data.closing.finalNode) || null;
1894
+ if (fin && fin.messageId === messageId) return f(fin);
1895
+ }
1896
+ return 0;
1897
+ }) || 0;
1898
+ };
1899
+ msgTime = pick(function (fin) { return fin.time || 0; });
1900
+ stepStart = pick(function (fin) { return (fin.timing && fin.timing.stepStartTime) || 0; });
1901
+ } catch (e) {}
1902
+ }
1903
+
1904
+ React.useEffect(function () {
1905
+ var from = stepStart || (msgTime ? (msgTime - 5 * 60 * 1000) : 0);
1906
+ var to = msgTime || 0;
1907
+ if (!sessionId) { setData({ status: "idle", data: null }); return; }
1908
+ setData({ status: "loading", data: s1.data });
1909
+ api({ action: "tokenForMessage", sessionId: sessionId, from: from, to: to }).then(function (res) {
1910
+ setData({ status: res && res.ok ? "done" : "error", data: res });
1911
+ }).catch(function () { setData({ status: "error", data: null }); });
1912
+ }, [messageId, sessionId, msgTime, stepStart]);
1913
+
1914
+ var convo = s1.status === "done" && s1.data && s1.data.conversation ? s1.data.conversation : null;
1915
+ var agg2 = s1.status === "done" && s1.data && s1.data.aggregate ? s1.data.aggregate : null;
1916
+ var recs = s1.status === "done" && s1.data && s1.data.records ? s1.data.records : [];
1917
+ var convoTokens = convo ? (convo.input + convo.cacheRead + convo.cacheWrite + convo.output + convo.reasoning) : 0;
1918
+ var turnTokens = agg2 ? (agg2.input + agg2.cacheRead + agg2.cacheWrite + agg2.output + agg2.reasoning) : 0;
1919
+ var label = t("获取中…");
1920
+ if (s1.status === "error") label = "—";
1921
+ if (s1.status === "done") {
1922
+ if (agg2) label = t("本轮 token") + " " + fmtInt(turnTokens);
1923
+ else if (convo) label = t("本轮 token") + " " + fmtInt(convoTokens);
1924
+ else label = "—";
1925
+ }
1926
+
1927
+ // 按本轮记录聚合模型
1928
+ var byModel = {};
1929
+ for (var bi = 0; bi < recs.length; bi++) {
1930
+ var br = recs[bi];
1931
+ var mk = br.model || br.modelKey || "unknown";
1932
+ var m = byModel[mk];
1933
+ if (!m) { m = byModel[mk] = { model: mk, calls: 0, input: 0, cacheRead: 0, cacheWrite: 0, output: 0, reasoning: 0, peakCost: 0, offCost: 0, totalCost: 0 }; }
1934
+ m.calls++;
1935
+ m.input += br.inputTokens; m.cacheRead += br.cacheReadTokens; m.cacheWrite += br.cacheWriteTokens;
1936
+ m.output += br.outputTokens; m.reasoning += br.reasoningTokens;
1937
+ if (br.peak) m.peakCost += br.autoCost; else m.offCost += br.autoCost;
1938
+ m.totalCost += br.autoCost;
1939
+ }
1940
+ var modelRows = [];
1941
+ for (var mk2 in byModel) modelRows.push(byModel[mk2]);
1942
+
1943
+ function stat(label2, value2) {
1944
+ return el("div", { style: __tok.stat },
1945
+ el("div", { style: __tok.statLabel }, label2),
1946
+ el("div", { style: __tok.statValue }, value2)
1947
+ );
1948
+ }
1949
+
1950
+ function cacheBar() {
1951
+ if (!agg2) return null;
1952
+ var pct = Math.min(100, Math.max(0, agg2.hitRate));
1953
+ return el("div", { style: __tok.barWrap },
1954
+ el("div", { style: __tok.barTrack },
1955
+ el("div", { style: Object.assign({}, __tok.barFill, { width: pct + "%" }) })
1956
+ ),
1957
+ el("span", { style: __tok.barLabel }, t("缓存命中率") + " " + (pct ? pct.toFixed(1) : "0") + "%")
1958
+ );
1959
+ }
1960
+
1961
+ function models() {
1962
+ if (!recs.length) return null;
1963
+ modelRows.sort(function (x, y) { return (y.totalCost - x.totalCost) || (y.calls - x.calls); });
1964
+ var MAX_MODELS = 6;
1965
+ var shown = modelRows.slice(0, MAX_MODELS);
1966
+ var extra = modelRows.length - shown.length;
1967
+ var cards = shown.map(function (m, idx) {
1968
+ return el("div", { key: String(idx), style: __tok.mCard },
1969
+ el("div", { style: __tok.mHead },
1970
+ el("span", { style: __tok.mName, title: m.model }, m.model),
1971
+ el("span", { style: __tok.mCost }, fmtMoney(m.totalCost))
1972
+ ),
1973
+ el("div", { style: __tok.mLine },
1974
+ t("输入·未命中") + " " + fmtInt(m.input) + " · " + t("缓存命中") + " " + fmtInt(m.cacheRead) + " · " + t("输出") + " " + fmtInt(m.output) + (m.reasoning ? " · " + t("推理") + " " + fmtInt(m.reasoning) : "")
1975
+ ),
1976
+ (m.peakCost > 0 || m.offCost > 0) ? el("div", { style: __tok.mLine },
1977
+ el("span", { style: { color: "#e08700" } }, t("峰") + " " + fmtMoney(m.peakCost)),
1978
+ " · ",
1979
+ el("span", { style: { color: "#3d6bd6" } }, t("谷") + " " + fmtMoney(m.offCost))
1980
+ ) : null
1981
+ );
1982
+ });
1983
+ if (extra > 0) cards.push(el("div", { key: "more-models", style: __tok.hint }, t("…及另外 ") + fmtInt(extra) + t(" 个模型(完整列表见「用量与消耗」页签)")));
1984
+ return el("div", null, cards);
1985
+ }
1986
+ function turnStats() {
1987
+ if (!agg2) return null;
1988
+ // 记录只存开始时间,单条调用时 min/max 相同会算成 0;本轮耗时直接用
1989
+ // 该消息所在 step 的起止(完成时间 − step 开始时间),才是真实耗时。
1990
+ var span = (msgTime && stepStart) ? (msgTime - stepStart) : ((agg2.time && agg2.time.end) ? Math.max(0, agg2.time.end - agg2.time.start) : 0);
1991
+ return el("div", { style: __tok.statRow },
1992
+ stat(t("本次输出"), fmtInt(agg2.output)),
1993
+ stat(t("本轮 token"), fmtInt(turnTokens)),
1994
+ stat(t("本轮消耗"), fmtMoney(agg2.totalCost)),
1995
+ stat(t("本轮耗时"), fmtDur(span)),
1996
+ stat(t("本轮缓存命中率"), (agg2.hitRate ? agg2.hitRate.toFixed(1) : "0") + "%")
1997
+ );
1998
+ }
1999
+
2000
+ function summary() {
2001
+ if (!convo) return null;
2002
+ var span = (convo.time && convo.time.end) ? Math.max(0, convo.time.end - convo.time.start) : 0;
2003
+ return el("div", { style: __tok.statRow },
2004
+ stat(t("总 token"), fmtInt(convoTokens)),
2005
+ stat(t("总消耗"), fmtMoney(convo.totalCost)),
2006
+ stat(t("耗时"), fmtDur(span)),
2007
+ stat(t("缓存命中率"), (convo.hitRate ? convo.hitRate.toFixed(1) : "0") + "%")
2008
+ );
2009
+ }
2010
+
2011
+ function body() {
2012
+ if (s1.status === "loading") return el("div", { style: __tok.hint }, t("获取中…"));
2013
+ if (s1.status === "error") return el("div", { style: __tok.hint }, t("查询失败"));
2014
+ if (!convo && !agg2) return el("div", { style: __tok.hint }, t("无 Token 数据"));
2015
+ var zero = convo ? (convoTokens === 0 && convo.totalCost === 0) : (turnTokens === 0 && agg2.totalCost === 0);
2016
+ if (zero) return el("div", { style: __tok.hint }, t("本期无消耗(所有用量为 0)"));
2017
+ return el("div", null,
2018
+ convo ? el("div", null,
2019
+ el("div", { style: __tok.title2 }, t("对话累计")),
2020
+ summary()
2021
+ ) : null,
2022
+ el("div", { style: __tok.title2 }, t("本轮明细")),
2023
+ turnStats(),
2024
+ cacheBar(),
2025
+ recs.length ? el("div", null,
2026
+ el("div", { style: __tok.title2 }, t("按模型")),
2027
+ models()
2028
+ ) : el("div", { style: __tok.hint }, t("本轮无记录")),
2029
+ el("div", { style: __tok.hint }, t("详细记录请在「用量与消耗」页签查看"))
2030
+ );
2031
+ }
2032
+
2033
+ var overlayEl = open ? el("div", { key: "usage-tok-overlay", style: __tok.overlay, onClick: function () { setOpen(false); } },
2034
+ el("div", { style: __tok.card, "data-dsh-usage-tok": "1", onClick: function (e) { e.stopPropagation(); } },
2035
+ el("div", { style: __tok.title },
2036
+ el("span", null, t("Token 明细")),
2037
+ el("button", { style: __tok.close, onClick: function () { setOpen(false); } }, t("关闭"))
2038
+ ),
2039
+ body()
2040
+ )
2041
+ ) : null;
2042
+ if (overlayEl && ReactDOM && typeof ReactDOM.createPortal === "function" && typeof document !== "undefined" && document.body) {
2043
+ overlayEl = ReactDOM.createPortal(overlayEl, document.body);
2044
+ }
2045
+ return el("div", { style: { display: "inline-flex", alignItems: "center", gap: 4 } },
2046
+ el("button", { type: "button", key: "usage-tok",
2047
+ style: Object.assign({}, st.btn, { padding: "0 6px", minWidth: 0, fontSize: fs(11) }),
2048
+ title: t("Token 明细"),
2049
+ onClick: function () { setOpen(true); }
2050
+ }, label),
2051
+ overlayEl
2052
+ );
2053
+ }
2054
+
1753
2055
  // ── plugin ──
1754
2056
  var inject = ["slots"];
1755
2057
 
@@ -1757,37 +2059,48 @@ var api = function (payload) {
1757
2059
  var slots = ctx.get("slots");
1758
2060
  if (slots === undefined) return;
1759
2061
  var timer = ctx.get("timer");
2062
+ // 语言跟随宿主设置(通用设置 → 语言):读取当前语言并订阅变化,切换后即时生效
2063
+ var locale = ctx.get("locale");
2064
+ if (locale) syncWithSystemLocale(locale);
2065
+ if (ctx.on) ctx.on("locale/change", function (snap) { setLang(snap && snap.active === "zh" ? "zh" : "en"); });
1760
2066
 
1761
2067
  slots.inject("conversation.view", function () {
1762
2068
  return slots.register(
1763
- { name: "conversation.view", id: "usage-cost-view", order: 20, label: t("用量与消耗") },
2069
+ { name: "conversation.view", id: "usage-cost-view", order: 20, label: function () { return t("用量与消耗"); } },
1764
2070
  function () { return el("div", { style: st.tab }, el(UsagePanel, { timer: timer })); }
1765
2071
  );
1766
2072
  });
1767
2073
 
1768
2074
  slots.inject("conversation.view", function () {
1769
2075
  return slots.register(
1770
- { name: "conversation.view", id: "balance-view", order: 30, label: t("剩余余额查询") },
2076
+ { name: "conversation.view", id: "balance-view", order: 30, label: function () { return t("剩余余额查询"); } },
1771
2077
  function () { return el("div", { style: st.tab }, el(BalancePanel, null)); }
1772
2078
  );
1773
2079
  });
1774
2080
 
1775
2081
  slots.inject("settings.section", function () {
1776
2082
  return slots.register(
1777
- { name: "settings.section", id: "usage-cost", order: 30, label: t("用量与消耗") },
2083
+ { name: "settings.section", id: "usage-cost", order: 30, label: function () { return t("用量与消耗"); } },
1778
2084
  function () { return el(UsagePanel, { timer: timer }); }
1779
2085
  );
1780
2086
  });
1781
2087
 
1782
2088
  slots.inject("settings.section", function () {
1783
2089
  return slots.register(
1784
- { name: "settings.section", id: "balance", order: 31, label: t("剩余余额查询") },
2090
+ { name: "settings.section", id: "balance", order: 31, label: function () { return t("剩余余额查询"); } },
1785
2091
  function () { return el(BalancePanel, null); }
1786
2092
  );
1787
2093
  });
2094
+
2095
+ slots.inject("conversation.chat.assistant-actions", function () {
2096
+ return slots.register(
2097
+ { name: "conversation.chat.assistant-actions", id: "usage-token", order: 8 },
2098
+ MessageTokenAction
2099
+ );
2100
+ });
1788
2101
  }
1789
2102
 
1790
- // test/debug surface: instant-switch guarantees and locale formatters
2103
+ // test/debug surface: instant-switch guarantees, locale formatters, peak/off-peak judgment
1791
2104
  exports.__i18n = {
1792
2105
  t: t,
1793
2106
  tb: tb,
@@ -1796,9 +2109,11 @@ var api = function (payload) {
1796
2109
  dayLabel: dayLabel,
1797
2110
  fmtMonthLabel: fmtMonthLabel,
1798
2111
  dailyStatsTitle: dailyStatsTitle,
1799
- weekdayLabels: weekdayLabels
2112
+ weekdayLabels: weekdayLabels,
2113
+ isPeakNow: isPeakNow,
2114
+ periodNow: periodNow
1800
2115
  };
1801
- // test/debug surface: instant-switch guarantees and locale formatters
2116
+ // test/debug surface: instant-switch guarantees, locale formatters, peak/off-peak judgment
1802
2117
  exports.__i18n = {
1803
2118
  t: t,
1804
2119
  tb: tb,
@@ -1807,7 +2122,9 @@ var api = function (payload) {
1807
2122
  dayLabel: dayLabel,
1808
2123
  fmtMonthLabel: fmtMonthLabel,
1809
2124
  dailyStatsTitle: dailyStatsTitle,
1810
- weekdayLabels: weekdayLabels
2125
+ weekdayLabels: weekdayLabels,
2126
+ isPeakNow: isPeakNow,
2127
+ periodNow: periodNow
1811
2128
  };
1812
2129
  exports.inject = inject;
1813
2130
  exports.apply = apply;