dsh-cost-meter 1.8.4 → 1.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  Per-conversation cost · daily totals · OpenCode Go subscription quota display · budget with usage percentage · official account balance · custom provider balance · balance progress bar · history · peak/off-peak pricing hours display (peak hours UTC 01:00–04:00, 06:00–10:00; weekends and Chinese public holidays are off-peak all day, with separate labels) · pre-switch popup & system-notification alerts for peak/off-peak changes (position / lead time / alert type configurable) · one-click price sync from the official docs · Codex-style token usage heat grid · multi-vendor model pricing (built-in 90+ model price catalog with auto-matching) · mainstream Coding Plan quota queries & display (Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow / CommandCode / SCNet / Volcano Ark / Qwen / Xiaomi MiMo) plan/API dual-track billing (subscription quota vs pay-as-you-go money separated, per-1% & full-window token/equivalent-cost estimates with daily/weekly/monthly curves) · · quota strip above the input box (budget / Go / coding-plan usage in one row, toggleable)
10
10
 
11
- [![version](https://img.shields.io/badge/version-1.8.4-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
11
+ [![version](https://img.shields.io/badge/version-1.8.5-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
12
12
 
13
- **v1.8.4** keeps **Cost details** beside the turns/speed and token/cache-hit statistics below the input box, without an extra budget row. Desktop installation now defaults to a pinned npm package to avoid the host error when re-adding an unchanged Git URL. See the [statistics guide](docs/billing-statistics.md#english) and [release notes](docs/release-notes/v1.8.4.md).
13
+ **v1.8.5** aligns statistics with DSH's native theme, adds full-selection step shares and expandable turn inputs/tool calls, and captures native-search usage when the fetch transport emits no Undici diagnostics. See the [statistics guide](docs/billing-statistics.md#english) and [release notes](docs/release-notes/v1.8.5.md).
14
14
 
15
15
  Desktop users: follow the [Desktop installation instructions](docs/install-troubleshooting.md#desktop-安装与更新) for the application's own CLI and `desktop` Profile.
16
16
 
@@ -320,22 +320,22 @@ On Node.js 20, use `npm install -g pnpm@10` instead. See [pnpm installation and
320
320
  dsh plugin --profile web add dsh-cost-meter
321
321
  ```
322
322
 
323
- **PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.8.4`** — review the script before running):
323
+ **PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.8.5`** — review the script before running):
324
324
 
325
325
  ```powershell
326
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.4/install.ps1 | iex
326
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.5/install.ps1 | iex
327
327
  ```
328
328
 
329
329
  **Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
330
330
 
331
331
  ```sh
332
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.4
332
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.5
333
333
  ```
334
334
 
335
335
  Without git, use the GitHub tag archive:
336
336
 
337
337
  ```sh
338
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.4.tar.gz
338
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.5.tar.gz
339
339
  ```
340
340
 
341
341
  After installing, **restart** `dsh web` (plugin rows, the Typert manifest and the client bundle are all scanned at startup):
package/README.zh-CN.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  本会话费用 · 当日费用 · OpenCode Go 订阅额度显示 · 预算与已用百分比 · 官方账户余额 · 自定义 Provider 余额查询(可配任意 HTTP 端点) · 余额三段进度条 · 历史记录 · 峰谷计价时段显示(UTC 01:00–04:00、06:00–10:00 为峰时段;周末与中国法定假日全天按谷价,分别标注) · 峰/谷切换前弹窗与系统通知提醒(位置/提前量/提醒类型可配) · 官方价格一键同步 · 类 Codex Token 用量热图 · 多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配) · 主流 Coding Plan 额度查询与显示(Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow / CommandCode / SCNet / 火山方舟 / 千问 / 小米 MiMo 十一家,含 Volcano Ark AK/SK 签名与 MiMo 控制台 Cookie 查询) · Plan/API 双轨计费(订阅额度与按量金额分离统计,每 1% 额度与满窗的 token/等值金额估算及日/周/月曲线) · 输入框上方额度横条(预算/Go/Coding Plan 用量一条横排显示,可开关)
10
10
 
11
- [![version](https://img.shields.io/badge/version-1.8.4-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
11
+ [![version](https://img.shields.io/badge/version-1.8.5-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
12
12
 
13
- **v1.8.4** 将输入框下方的「费用明细」与轮数、速度、Token 和缓存命中率排在同一行,预算不再独占一行。Desktop 安装默认使用固定 npm 正式版,避免重复 Git 安装的宿主识别错误。详见[计费统计说明](docs/billing-statistics.md#中文)及[更新说明](docs/release-notes/v1.8.4.md)。
13
+ **v1.8.5** 按 DSH 原生风格调整统计界面,增加全部调用的步骤占比,以及可展开的每轮输入、工具参数和结果;原生搜索新增不依赖 Undici 诊断事件的用量捕获。详见[计费统计说明](docs/billing-statistics.md#简体中文)及[更新说明](docs/release-notes/v1.8.5.md)。
14
14
 
15
15
  桌面端用户请按 [Desktop 安装说明](docs/install-troubleshooting.md#desktop-安装与更新),使用应用自带的 CLI 和 `desktop` Profile。
16
16
 
@@ -320,22 +320,22 @@ Node.js 20 请改用 `npm install -g pnpm@10`。版本要求见 [pnpm 官方安
320
320
  dsh plugin --profile web add dsh-cost-meter
321
321
  ```
322
322
 
323
- **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.8.4`**,建议先下载审阅再运行):
323
+ **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.8.5`**,建议先下载审阅再运行):
324
324
 
325
325
  ```powershell
326
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.4/install.ps1 | iex
326
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.8.5/install.ps1 | iex
327
327
  ```
328
328
 
329
329
  **或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
330
330
 
331
331
  ```sh
332
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.4
332
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.8.5
333
333
  ```
334
334
 
335
335
  没有 git 时可用 GitHub tag 打包直链:
336
336
 
337
337
  ```sh
338
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.4.tar.gz
338
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.8.5.tar.gz
339
339
  ```
340
340
 
341
341
  安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
@@ -10,6 +10,9 @@ Open a conversation and click **Cost details** in the same row as turns, speed,
10
10
  - **Conversation detail:** Input, output, cache read, cache write, and reasoning costs; individual model, compaction and native-search calls; tokens × price per million; long-context rates and unavailable prices. Small amounts use at least eight decimal places. Fifty calls per page remain part of the complete detail total.
11
11
 
12
12
  - **Turns and call types:** Full-selection totals by logged turn number and by model / compaction / native-search call. Turn pages contain 25 complete groups, independently of the 50-call pages. Calls without a turn number remain in an explicit unassigned group. Expand a call to see its logged turn and step, routing and price formula.
13
+ - **Step shares:** Horizontal bars show cost components, call-type counts and each logged step's share. Switch between costs and call counts. The denominator covers all filtered calls, across pages; the top 12 steps plus a combined remainder preserve the full amount. Tools are not counted as additional model calls. No recorded step stays explicitly unassigned.
14
+ - **Turn inspection:** Expand a turn to load its original user inputs, then expand each tool to see arguments, result and completion/error status. Tool records paginate 20 at a time; each long text field shows up to 16,000 characters with an explicit notice. Attachments show their type; system prompts, request headers and binary data are excluded. This is the whole turn, independent of the billing filters. Opening the overview does not load these contents.
15
+ - **Appearance:** Native DSH theme tokens, neutral surfaces, light borders, compact segmented controls and collapsible explanations replace the earlier colored dashboard cards. Light/dark themes and narrow windows share the same layout rules.
13
16
 
14
17
  ### What the numbers mean
15
18
 
@@ -33,6 +36,9 @@ The statistics screen uses DSH's package-local asynchronous module loader. Since
33
36
  - **单对话:**输入、输出、缓存读取、缓存写入、推理费用,以及模型调用、上下文压缩、原生搜索的逐次明细。每次调用列出 Token × 每百万 Token 单价、金额、长上下文档位及缺价提示。小额费用至少保留八位小数。每页显示 50 次调用,分页不影响完整明细合计。
34
37
 
35
38
  - **轮次和调用类型:**按日志中的轮次编号,以及模型调用、上下文压缩、原生搜索汇总。每页 25 个完整轮次,与每页 50 次调用分别翻页;一轮跨多个调用页时仍显示完整合计。日志没有轮次编号的调用单列为「未标明轮次」。逐次明细标明轮次、步骤、路由和价格公式。
39
+ - **步骤占比:**横向条形图展示费用构成、调用类型次数和各步骤占比,可切换费用或调用次数。分母覆盖筛选后的全部调用,跨页合计;显示前 12 个步骤,其余合并,保留完整金额与次数。没有步骤编号的调用单列;工具执行不额外算作模型调用。
40
+ - **展开轮次:**点击一轮才读取其用户输入和工具调用,继续展开工具可查看参数、结果、完成或失败状态。工具每页 20 条,单段文本最多显示 16,000 个字符并标明截断;附件只显示类型,不读取系统提示、请求头或二进制内容。这里显示整轮原始记录,不受计费筛选影响。打开统计首页不会加载这些正文。
41
+ - **界面:**使用 DSH 的主题颜色、轻边框、紧凑分段按钮和可折叠说明,替换原来的彩色顶部卡片;支持浅色、深色和窄窗口。
36
42
 
37
43
  ### 统计口径
38
44
 
@@ -1,19 +1,23 @@
1
- window.__ModuleLoader__.load({id:"dsh-cost-meter",chunk:"client.statistics.js",factory:ht=>{const ft=ht("react"),{createElement:t,useState:k,useEffect:vt,Fragment:Q}=ft,yt=["input","output","cacheRead","cacheWrite","reasoning","calls","cost","apiCost"],S=(s,l)=>l==="total"?s.cost:l==="plan"?Math.max(0,s.cost-s.apiCost):s.apiCost,et=s=>s.input+s.cacheRead+s.cacheWrite+s.output,z=(s,l)=>l>0?(100*s/l).toFixed(1)+"%":"—",xt=(s,l)=>new Date(Date.parse(s)+l*864e5).toISOString().slice(0,10),H=s=>l=>{if(typeof l!==s||s==="number"&&!Number.isFinite(l))throw new Error("Invalid statistics "+s);return l},b=H("number"),r=H("string"),A=H("boolean"),K=s=>s===null?null:b(s),P=s=>l=>{if(!Array.isArray(l))throw new Error("Invalid statistics array");return l.map(s)},w=s=>l=>{if(!l||typeof l!="object"||Array.isArray(l))throw new Error("Invalid statistics object");return Object.fromEntries(Object.entries(s).map(([o,d])=>[o,d(l[o])]))},q=Object.fromEntries(yt.map(s=>[s,b])),st=w(q),nt=w({provider:r,model:r,bucket:r,tokens:b,rate:b,cost:b,priced:A,plan:A}),kt=w({kind:r,turn:K,step:K,provider:r,model:r,atMs:b,cost:b,apiCost:b,plan:A,priced:A,longContext:A,rows:P(nt)}),V=w({from:r,to:r,retainedFrom:r,retainedTo:r,totals:st,days:P(w({...q,date:r})),models:P(w({...q,key:r,provider:r,model:r,priced:A})),sessions:P(w({...q,id:r,title:r})),providers:P(r),modelOptions:P(r),sessionCount:b,offset:b,unassignedCost:b,unmodeledCost:b}),G=w({found:A,cost:b,apiCost:b,rows:P(nt),calls:P(kt),totalCalls:b,offset:b,recorded:st,kinds:P(w({...q,kind:r,unpriced:A})),turns:P(w({...q,turn:K,unpriced:A})),totalTurns:b,turnOffset:b}),lt=s=>{const l=w({from:r,to:r,provider:r,model:r,sessionId:r,basis:r,offset:b})(s);if(l.turnOffset=b(s.turnOffset??0),!["api","plan","total"].includes(l.basis)||![l.offset,l.turnOffset].every(o=>Number.isSafeInteger(o)&&o>=0&&o<=1e7))throw new Error("Invalid statistics query");return l},ct=(s,l)=>{const o={parse:l};return{mode:"strict",typeSymbol:"dsh-cost-meter#"+s,schema:o,create:()=>o}},ot={package:"dsh-cost-meter/statistics",descriptors:[["getBillingStatistics","BillingStatistics",V],["getSessionBilling","SessionBilling",G]].map(([s,l,o])=>({id:"dsh-cost-meter#costMeter/"+s,service:"costMeter",namespace:"costMeter",method:s,invocation:{kind:"direct"},parameters:[{name:"query",wire:"query",source:"json",codec:ct("StatisticsQuery",lt)}],result:ct(l,o)}))},Nt=`
2
- .cm-stat{--cm-accent:#367dce;--cm-plan:#a16aca;--cm-muted:var(--dsw-alias-label-tertiary,#747f8e);color:var(--dsw-alias-label-primary,#1e293b);font-size:13px;line-height:1.5;container-type:inline-size}
3
- .cm-stat *{box-sizing:border-box}.cm-stat h2,.cm-stat h3,.cm-stat p{margin:0}.cm-stat h2{font-size:24px;letter-spacing:-.6px}.cm-stat h3{font-size:14px;font-weight:650}
4
- .cm-stat-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:10px 0 20px}.cm-stat-sub{font-size:12px;color:var(--cm-muted)}
1
+ window.__ModuleLoader__.load({id:"dsh-cost-meter",chunk:"client.statistics.js",factory:xt=>{const kt=xt("react"),{createElement:t,useState:y,useEffect:Nt,Fragment:Y}=kt,Ct=["input","output","cacheRead","cacheWrite","reasoning","calls","cost","apiCost"],T=(s,r)=>r==="total"?s.cost:r==="plan"?Math.max(0,s.cost-s.apiCost):s.apiCost,Z=s=>s.input+s.cacheRead+s.cacheWrite+s.output,j=(s,r)=>r>0?(100*s/r).toFixed(1)+"%":"—",wt=(s,r)=>new Date(Date.parse(s)+r*864e5).toISOString().slice(0,10),tt=s=>r=>{if(typeof r!==s||s==="number"&&!Number.isFinite(r))throw new Error("Invalid statistics "+s);return r},g=tt("number"),p=tt("string"),N=tt("boolean"),D=s=>s===null?null:g(s),I=s=>r=>{if(!Array.isArray(r))throw new Error("Invalid statistics array");return r.map(s)},k=s=>r=>{if(!r||typeof r!="object"||Array.isArray(r))throw new Error("Invalid statistics object");return Object.fromEntries(Object.entries(s).map(([i,m])=>[i,m(r[i])]))},q=Object.fromEntries(Ct.map(s=>[s,g])),it=k(q),ot=k({provider:p,model:p,bucket:p,tokens:g,rate:g,cost:g,priced:N,plan:N}),St=k({kind:p,turn:D,step:D,provider:p,model:p,atMs:g,cost:g,apiCost:g,plan:N,priced:N,longContext:N,rows:I(ot)}),at=k({from:p,to:p,retainedFrom:p,retainedTo:p,totals:it,days:I(k({...q,date:p})),models:I(k({...q,key:p,provider:p,model:p,priced:N})),sessions:I(k({...q,id:p,title:p})),providers:I(p),modelOptions:I(p),sessionCount:g,offset:g,unassignedCost:g,unmodeledCost:g}),et=k({found:N,cost:g,apiCost:g,rows:I(ot),calls:I(St),totalCalls:g,offset:g,recorded:it,kinds:I(k({...q,kind:p,unpriced:N})),turns:I(k({...q,turn:D,unpriced:N})),totalTurns:g,turnOffset:g,stepShares:k(Object.fromEntries(["cost","calls"].map(s=>[s,I(k({...q,turn:D,step:D,other:N,unpriced:N}))])))}),st=k({found:N,turn:g,input:p,inputTruncated:N,totalTools:g,offset:g,tools:I(k({seq:g,step:D,name:p,callId:p,atMs:g,arguments:p,result:p,truncated:N,status:p}))}),mt=s=>{const r=k({sessionId:p,turn:g,offset:g})({...s,offset:s.offset??0});if(!r.sessionId||r.sessionId.length>512||![r.turn,r.offset].every(i=>Number.isSafeInteger(i)&&i>=0)||r.offset>1e7)throw new Error("Invalid turn inspection query");return r},dt=s=>{const r=k({from:p,to:p,provider:p,model:p,sessionId:p,basis:p,offset:g})(s);if(r.turnOffset=g(s.turnOffset??0),!["api","plan","total"].includes(r.basis)||![r.offset,r.turnOffset].every(i=>Number.isSafeInteger(i)&&i>=0&&i<=1e7))throw new Error("Invalid statistics query");return r},pt=(s,r)=>{const i={parse:r};return{mode:"strict",typeSymbol:"dsh-cost-meter#"+s,schema:i,create:()=>i}},ut={package:"dsh-cost-meter/statistics",descriptors:[["getBillingStatistics","BillingStatistics",at],["getSessionBilling","SessionBilling",et],["getTurnInspection","TurnInspection",st,mt]].map(([s,r,i,m])=>({id:"dsh-cost-meter#costMeter/"+s,service:"costMeter",namespace:"costMeter",method:s,invocation:{kind:"direct"},parameters:[{name:"query",wire:"query",source:"json",codec:pt(m?"TurnInspectionQuery":"StatisticsQuery",m??dt)}],result:pt(r,i)}))},Tt=`
2
+ .cm-stat{--cm-accent:var(--dsw-alias-state-business-primary,#4d6bfe);--cm-plan:var(--dsw-alias-label-tertiary,#8b9099);--cm-muted:var(--dsw-alias-label-tertiary,#858a94);--cm-border:var(--dsw-alias-border-l3,#e9eaed);--cm-surface:var(--dsw-alias-bg-layer-1,#f7f8fa);color:var(--dsw-alias-label-primary,#25262b);font-family:var(--ds-font-family-sans,inherit);font-size:13px;line-height:1.55;container-type:inline-size}
3
+ .cm-stat *{box-sizing:border-box}.cm-stat h2,.cm-stat h3,.cm-stat p{margin:0}.cm-stat h2{font-size:18px;font-weight:600;letter-spacing:0}.cm-stat h3{font-size:14px;font-weight:500}
4
+ .cm-stat-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:0 0 18px}.cm-stat-sub{font-size:12px;color:var(--cm-muted)}
5
5
  .cm-stat button,.cm-stat select,.cm-stat input{font:inherit;color:inherit}.cm-stat button{cursor:pointer}.cm-stat button:disabled{cursor:default;opacity:.45}.cm-stat button:focus-visible,.cm-stat select:focus-visible,.cm-stat input:focus-visible{outline:2px solid var(--cm-accent);outline-offset:3px}
6
6
  .cm-stat-controls{display:flex;flex-wrap:wrap;gap:10px;align-items:end;margin:14px 0}.cm-stat-controls label{display:grid;gap:4px;font-size:11px;color:var(--cm-muted)}
7
- .cm-stat input,.cm-stat select{background:var(--dsw-alias-bg-layer-2,#f5f7fa);border:1px solid var(--dsw-alias-border-l1,#e0e5ed);border-radius:7px;padding:7px 9px;max-width:210px;min-width:0}
8
- .cm-stat-periods{display:flex;flex-wrap:wrap;gap:4px}.cm-stat-btn{border:1px solid var(--dsw-alias-border-l1,#dce3ec);border-radius:7px;background:transparent;padding:6px 11px}.cm-stat-btn[aria-pressed=true]{background:var(--cm-accent);color:white;border-color:var(--cm-accent)}
9
- .cm-stat-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:18px 0}.cm-stat-metric{border-top:3px solid var(--cm-accent);padding:14px 16px;background:var(--dsw-alias-bg-layer-2,#f5f7fa);border-radius:4px 4px 10px 10px}.cm-stat-metric:nth-child(2){border-color:var(--cm-plan)}
10
- .cm-stat-value{font-variant-numeric:tabular-nums;font-size:clamp(17px,2.7cqw,28px);font-weight:650;letter-spacing:-.6px;margin:7px 0;overflow-wrap:anywhere}.cm-stat-metric .cm-stat-sub{font-size:11px}
11
- .cm-stat-panel{border:1px solid var(--dsw-alias-border-l1,#dce3ec);border-radius:12px;padding:18px;margin:14px 0;min-width:0}.cm-stat-panel-head{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:16px}
7
+ .cm-stat input,.cm-stat select{background:var(--dsw-alias-bg-base,#fff);border:.5px solid var(--cm-border);border-radius:var(--dsw-radius-sm,8px);padding:6px 9px;max-width:210px;min-width:0}
8
+ .cm-stat-periods{display:flex;flex-wrap:wrap;gap:2px;background:var(--cm-surface);border-radius:var(--dsw-radius-lg,12px);padding:3px}.cm-stat-btn{border:.5px solid var(--cm-border);border-radius:var(--dsw-radius-sm,8px);background:transparent;padding:5px 10px}.cm-stat-btn:hover{background:var(--dsw-alias-interactive-bg-hover,#f0f1f4)}.cm-stat-periods .cm-stat-btn{border-color:transparent;color:var(--cm-muted)}.cm-stat-btn[aria-pressed=true]{background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#25262b);border-color:var(--cm-border);box-shadow:0 1px 2px #00000005}
9
+ .cm-stat-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:0;margin:20px 0;padding:16px 0;border-block:1px solid var(--cm-border)}.cm-stat-metric{padding:0 18px;border-right:1px solid var(--cm-border)}.cm-stat-metric:first-child{padding-left:0}.cm-stat-metric:last-child{border:0}
10
+ .cm-stat-value{font-variant-numeric:tabular-nums;font-size:clamp(18px,2.5cqw,26px);font-weight:500;letter-spacing:-.5px;margin:5px 0;overflow-wrap:anywhere}.cm-stat-metric .cm-stat-sub{font-size:11px}
11
+ .cm-stat-panel{border:1px solid var(--cm-border);border-radius:var(--dsw-radius-lg,12px);padding:16px;margin:16px 0;min-width:0}.cm-stat-panel-head{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:14px}
12
12
  .cm-stat-chart{height:170px;display:flex;align-items:end;gap:clamp(2px,.5cqw,7px);border-bottom:1px solid var(--dsw-alias-border-l1,#dce3ec);background:repeating-linear-gradient(to top,transparent 0,transparent 41px,var(--dsw-alias-border-l1,#e9edf3) 42px,transparent 43px);padding:0 2px}
13
13
  .cm-stat-bar{height:100%;flex:1;min-width:1px;display:flex;align-items:end;justify-content:center;background:transparent;border:0;padding:0;position:relative}.cm-stat-bar>span{display:block;width:100%;max-width:45px;border-radius:4px 4px 0 0;background:var(--cm-accent);min-height:2px;opacity:.83}.cm-stat-bar:hover>span,.cm-stat-bar[aria-pressed=true]>span{opacity:1;background:var(--cm-plan)}
14
14
  .cm-stat-axis{display:flex;justify-content:space-between;font-size:11px;color:var(--cm-muted);margin-top:6px}.cm-stat-grid{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr);gap:14px}.cm-stat-grid>.cm-stat-panel{margin:0}
15
15
  .cm-stat-rank{list-style:none;padding:0;margin:0}.cm-stat-rank li{margin-top:14px}.cm-stat-rank button{width:100%;border:0;background:transparent;text-align:left;padding:0;color:inherit}.cm-stat-rankline{display:flex;justify-content:space-between;gap:10px}.cm-stat-rankline>span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cm-stat-track{height:5px;margin-top:7px;background:var(--dsw-alias-bg-layer-2,#f0f3f7);border-radius:4px;overflow:hidden}.cm-stat-track>span{display:block;height:100%;background:var(--cm-accent);border-radius:4px}.cm-stat-number{font-variant-numeric:tabular-nums;white-space:nowrap}
16
16
  .cm-stat-table{width:100%;border-collapse:collapse;font-size:12px}.cm-stat-table th,.cm-stat-table td{padding:11px 9px;border-bottom:1px solid var(--dsw-alias-border-l1,#e0e5ed);text-align:right;font-variant-numeric:tabular-nums}.cm-stat-table th{font-weight:500;color:var(--cm-muted)}.cm-stat-table th:first-child,.cm-stat-table td:first-child{text-align:left}.cm-stat-table button{color:var(--cm-accent);border:0;background:transparent;text-align:left;padding:0;max-width:300px;overflow-wrap:anywhere}.cm-stat-scroll{overflow:auto}
17
17
  .cm-stat-page{display:flex;gap:12px;align-items:center;justify-content:flex-end;margin-top:14px}.cm-stat-note{padding:10px 12px;background:var(--dsw-alias-bg-layer-2,#f5f7fa);border-radius:8px;margin:12px 0!important;font-size:12px;color:var(--cm-muted)}.cm-stat-error{color:var(--dsw-alias-state-error-primary,#c75040);white-space:pre-wrap}.cm-stat-empty{padding:30px;text-align:center;color:var(--cm-muted)}.cm-stat-call{padding:11px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#dce3ec)}.cm-stat-call summary{cursor:pointer;overflow-wrap:anywhere}.cm-stat-call p{overflow-wrap:anywhere;margin-top:6px}
18
+ .cm-stat summary{cursor:pointer}.cm-stat-help{font-size:12px;color:var(--cm-muted);margin:10px 0}.cm-stat-help>p{margin-top:8px}.cm-stat-shares{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24px;margin:20px 0}.cm-stat-share-list{list-style:none;margin:12px 0 0;padding:0;display:grid;gap:11px}.cm-stat-share-head{display:flex;justify-content:space-between;gap:12px;font-size:12px}.cm-stat-share-head>span:first-child{min-width:0;overflow-wrap:anywhere}.cm-stat-share-value{white-space:nowrap;font-variant-numeric:tabular-nums}.cm-stat-share-track{height:6px;background:var(--cm-surface);border-radius:99px;overflow:hidden;margin-top:5px}.cm-stat-share-fill{display:block;height:100%;background:var(--cm-accent);border-radius:99px;opacity:.68}.cm-stat-share-list li:nth-child(even) .cm-stat-share-fill{opacity:.42}.cm-stat-share-list li[data-other=true] .cm-stat-share-fill{background:var(--cm-plan)}
19
+ .cm-stat-breakdown{padding-block:18px;border-block:1px solid var(--cm-border);margin-block:18px}.cm-stat-turns{margin-top:24px}.cm-stat-turn{border-top:1px solid var(--cm-border)}.cm-stat-turn-toggle{display:flex;align-items:center;gap:10px;justify-content:space-between;width:100%;text-align:left;padding:12px 4px;background:transparent;border:0}.cm-stat-turn-toggle:hover{background:var(--dsw-alias-interactive-bg-hover,#f5f6f8)}.cm-stat-turn-toggle>span:first-child{font-weight:500}.cm-stat-turn-toggle>span:last-child{color:var(--cm-muted);font-size:12px;text-align:right;font-variant-numeric:tabular-nums}.cm-stat-inspection{background:var(--cm-surface);padding:16px;border-radius:10px;margin:0 0 12px}.cm-stat-inspection h4{font-size:12px;font-weight:500;margin:0 0 8px}.cm-stat-pre{font:12px/1.6 var(--ds-font-family-code,monospace);white-space:pre-wrap;overflow-wrap:anywhere;max-height:280px;overflow:auto;margin:8px 0 16px;padding:12px;border:1px solid var(--cm-border);background:var(--dsw-alias-bg-base,#fff);border-radius:8px}.cm-stat-tool{border-top:1px solid var(--cm-border);padding:10px 0}.cm-stat-tool summary{display:flex;flex-wrap:wrap;gap:8px;align-items:center;font-size:12px}.cm-stat-tool summary::before{content:'›';color:var(--cm-muted)}.cm-stat-tool[open] summary::before{content:'⌄'}.cm-stat-tool .cm-stat-sub{margin-left:auto}.cm-stat-truncated{color:var(--cm-muted);font-size:12px}.cm-stat-step-list{columns:2;column-gap:28px}.cm-stat-step-list li{break-inside:avoid;margin-bottom:12px}.cm-stat-step-list.cm-stat-share-list{display:block}
20
+ dialog:has(.cm-stat){padding:24px!important;border-color:var(--dsw-alias-border-l3,#e9eaed)!important;box-shadow:var(--dsw-elevation-prominent,0 20px 90px #0002)}dialog:has(.cm-stat)::backdrop{background:#0005}dialog:has(.cm-stat)>.cm-btn{border:0;border-radius:8px;background:transparent;font-size:20px;line-height:24px;width:28px;height:28px;padding:0;margin-left:12px}
21
+ @container(max-width:700px){.cm-stat-shares{grid-template-columns:1fr;gap:20px}.cm-stat-step-list{columns:1}.cm-stat-turn-toggle{align-items:flex-start}.cm-stat-turn-toggle>span:last-child{max-width:60%}.cm-stat-metric{padding:10px!important}.cm-stat-metric:nth-child(2){border:0}.cm-stat-page{gap:8px}}
18
22
  @container(max-width:700px){.cm-stat-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.cm-stat-grid{grid-template-columns:1fr}.cm-stat-panel{padding:12px}.cm-stat-head{align-items:start}.cm-stat-controls label{flex:1}.cm-stat input,.cm-stat select{max-width:100%;width:100%}.cm-stat-value{font-size:23px}}
19
- `;function it(s,l,o,d){const[m,c]=k({value:null,error:"",loading:!0}),a=JSON.stringify(o);return vt(()=>{let i=!0;return c({value:null,error:"",loading:!0}),s[l](JSON.parse(a)).then(y=>{i&&c({value:y,error:"",loading:!1})},y=>{i&&c({value:null,error:String(y.message??y),loading:!1})}),()=>{i=!1}},[s,l,a,d]),m}const B=(s,l,o={})=>t("button",{type:"button",className:"cm-stat-btn",onClick:l,...o},s);function X({offset:s,count:l,size:o,onChange:d,text:m}){return t("div",{className:"cm-stat-page"},t("span",{className:"cm-stat-sub"},l?`${s+1}–${Math.min(s+o,l)} / ${l}`:"0"),B(m("上一页","Previous"),()=>d(Math.max(0,s-o)),{disabled:!s}),B(m("下一页","Next"),()=>d(s+o),{disabled:s+o>=l}))}function rt({rows:s,money:l,text:o,onSelect:d,label:m}){const c=Math.max(0,...s.map(i=>i.value)),a=s.reduce((i,y)=>i+y.value,0);return t("div",null,t("div",{className:"cm-stat-panel-head"},t("h3",null,m),t("span",{className:"cm-stat-sub"},o("最高 ","Peak ")+l(c)+" · "+o("合计 ","Total ")+l(a))),t("div",{className:"cm-stat-chart",role:"group","aria-label":m},s.map((i,y)=>t("button",{key:y,type:"button",className:"cm-stat-bar",title:i.label+" · "+l(i.value),"aria-label":i.label+" · "+l(i.value),onClick:()=>d(i)},t("span",{style:{height:c?Math.max(1,i.value/c*100)+"%":"0%",opacity:i.value?void 0:.2}})))),t("div",{className:"cm-stat-axis"},t("span",null,s[0]?.label),t("span",null,s.at(-1)?.label)))}function dt(s,l){const o=Math.max(1,Math.ceil(s.length/60)),d=[];for(let m=0;m<s.length;m+=o){const c=s.slice(m,m+o),a=c[0].date,i=c.at(-1).date;d.push({from:a,to:i,label:a===i?a:a+" – "+i,value:c.reduce((y,O)=>y+S(O,l),0)})}return d}function mt({api:s,query:l,revision:o,money:d,formatTokens:m,text:c}){const[a,i]=k(0),[y,O]=k(-1),[W,C]=k(0),E=it(s,"getSessionBilling",{...l,offset:a,turnOffset:W},o),f=E.value,M=l.basis,R={input:c("输入","Input"),output:c("输出","Output"),cacheRead:c("缓存读取","Cache read"),cacheWrite:c("缓存写入","Cache write"),reasoning:c("推理","Reasoning")},j={model:c("模型调用","Model call"),compaction:c("上下文压缩","Compaction"),search:c("原生搜索","Native search")},x=n=>n==null?c("未标明轮次","Turn not recorded"):c("轮次 ","Turn ")+n;if(E.loading)return t("p",{className:"cm-stat-empty",role:"status"},c("正在读取本对话的用量明细…","Loading this conversation’s usage records…"));if(E.error)return t("p",{className:"cm-stat-error",role:"alert"},E.error);if(!f.found)return t("p",{className:"cm-stat-note"},c("没有可用的调用日志。上方账本统计仍然有效;明细不会按零费用处理。","Call logs are unavailable. The ledger totals above remain valid; missing details do not mean zero cost."));const Y=n=>M==="total"||(M==="plan"?n.plan:!n.plan),F=Object.keys(R).map(n=>{const u=f.rows.filter(v=>v.bucket===n&&Y(v));return{bucket:n,tokens:u.reduce((v,g)=>v+g.tokens,0),cost:u.reduce((v,g)=>v+g.cost,0),unpriced:u.some(v=>!v.priced)}}),T=S(f,M),L=S(f.recorded,M),Z=Math.abs(T-L)>Math.max(1e-8,Math.abs(L)*1e-8)||f.totalCalls!==f.recorded.calls,U=(n,u)=>t("p",{key:u,className:"cm-stat-sub"},R[n.bucket]+": "+n.tokens.toLocaleString()+" × "+(n.priced?d(n.rate):"?")+" / 1,000,000 = "+(n.priced?d(n.cost):"?")),_=(n,u,v)=>t("div",{className:"cm-stat-scroll",style:{marginTop:20}},t("h3",null,n),t("table",{className:"cm-stat-table"},t("thead",null,t("tr",null,...[n,c("调用次数","Calls"),c("输入 / 缓存 / 输出 Tokens","Input / cache / output tokens"),"API",c("Plan 等值","Plan equivalent")].map(g=>t("th",{key:g},g)))),t("tbody",null,u.map((g,D)=>t("tr",{key:D},t("td",null,v(g)),t("td",null,g.calls),t("td",null,[g.input,g.cacheRead+g.cacheWrite,g.output].map(m).join(" / ")),t("td",null,d(g.apiCost)+(g.unpriced?" + ?":"")),t("td",null,d(Math.max(0,g.cost-g.apiCost))+(g.unpriced?" + ?":"")))))));return t("section",{className:"cm-stat-panel"},t("div",{className:"cm-stat-panel-head"},t("h3",null,c("单对话费用明细","Conversation cost details")),t("span",{className:"cm-stat-sub"},c("只统计本对话自身的调用","Own calls only; child conversations are separate"))),t("p",{className:"cm-stat-note"},c("明细按日志中的调用时间、用量和当前配置的历史价格规则计算。上方汇总采用已入账金额;调整价格后两者可能不同。","Details use logged usage and call times with the currently configured historical price rules. The summary above uses recorded ledger amounts; changing prices can produce a difference.")),Z?t("p",{className:"cm-stat-note"},c("账本与可用明细不同:账本 ","Ledger and available details differ: ledger ")+d(L)+" / "+f.recorded.calls+c(" 次;明细 "," calls; details ")+d(T)+" / "+f.totalCalls+c(" 次。"," calls.")):null,t("div",{className:"cm-stat-scroll"},t("table",{className:"cm-stat-table"},t("thead",null,t("tr",null,...[c("费用构成","Cost component"),"Tokens",c("费用","Cost"),c("费用占比","Cost share")].map(n=>t("th",{key:n},n)))),t("tbody",null,F.map(n=>t("tr",{key:n.bucket},t("td",null,R[n.bucket]),t("td",null,n.tokens.toLocaleString()),t("td",null,d(n.cost)+(n.unpriced?" + ?":"")),t("td",null,z(n.cost,T))))))),t("p",{className:"cm-stat-sub",style:{marginTop:10}},c("推理 Token 可能包含在输出中;单价为 0 时不另收推理费。未定价用量显示 ?。","Reasoning tokens may overlap output; a zero reasoning rate adds no separate charge. Unpriced usage is marked ?.")),_(c("按调用类型统计","Cost by call type"),f.kinds,n=>j[n.kind]??n.kind),_(c("按轮次统计","Cost by turn"),f.turns,n=>x(n.turn)),t("p",{className:"cm-stat-sub"},c("轮次沿用日志编号;未记录轮次的调用单列。每轮包含其全部调用,跨调用分页也不会拆分。","Turn numbers come from the log; calls without one are listed separately. Each turn includes all its calls, across call pages.")),t(X,{offset:W,count:f.totalTurns,size:25,onChange:C,text:c}),t("div",{style:{marginTop:24}},t(rt,{rows:f.calls.map((n,u)=>({label:"#"+(a+u+1),index:u,value:S(n,M)})),money:d,text:c,label:c("逐次调用费用 · 当前页","Cost per call · current page"),onSelect:n=>{O(n.index),document.getElementById("cm-stat-call-"+n.index)?.scrollIntoView({block:"nearest",behavior:"smooth"})}})),f.calls.map((n,u)=>t("details",{key:u,id:"cm-stat-call-"+u,className:"cm-stat-call",open:y===u,onToggle:v=>{!v.currentTarget.open&&y===u&&O(-1)}},t("summary",null,"#"+(a+u+1)+" · "+x(n.turn)+(n.step==null?"":c(" / 步骤 "," / step ")+n.step)+" · "+(j[n.kind]??n.kind)+" · "+n.provider+" / "+n.model+" · "+(n.plan?"Plan ":"API ")+(n.priced?d(n.cost):"?")+(n.longContext?c(" · 长上下文价"," · long-context rate"):"")),t("p",{className:"cm-stat-sub"},new Date(n.atMs).toLocaleString()),n.rows.map(U))),t(X,{offset:a,count:f.totalCalls,size:50,onChange:n=>{i(n),O(-1)},text:c}))}function pt({state:s,api:l,sessionId:o="",formatMoneyUsd:d,formatTokens:m}){const c=s.config.locale==="en"||s.config.locale==="auto"&&!/^zh\b/i.test(navigator.language),a=(e,N)=>c?N:e,[i,y]=k(o?"all":"week"),[O,W]=k(null),[C,E]=k({id:o,title:o}),[f,M]=k(""),[R,j]=k(""),[x,Y]=k(o?"total":"api"),[F,T]=k(0),[L,Z]=k(0),[U,_]=k(!1),n=s.meta.dayKey||new Date().toISOString().slice(0,10),u=i==="all"?"":i==="custom"?O?.from||n:xt(n,i==="week"?-6:i==="month"?-29:0),v=i==="custom"&&O?.to||n,g={from:u,to:v,provider:f,model:R,basis:x,sessionId:C.id,offset:F},D=it(l,"getBillingStatistics",g,L+":"+s.meta.now),p=D.value,I=e=>d(e,{...s.config,decimals:Math.max(e!==0&&Math.abs(e)<.01?8:4,s.config.decimals??2)}),ut=e=>{y(e),T(0)},gt=e=>{E(e),T(0),M(""),j("")},wt=x==="total"?a("API + Plan 等值","API + Plan equivalent"):x==="plan"?a("Plan 等值","Plan equivalent"):a("API 费用","API cost"),tt=(e,N,at,Pt)=>t("label",null,e,t("select",{value:N,onChange:J=>{at(J.target.value),T(0)}},Pt.map(([J,Tt])=>t("option",{key:J,value:J},Tt)))),St={input:a("未缓存输入","Uncached input"),cacheRead:a("缓存读取","Cache read"),cacheWrite:a("缓存写入","Cache write"),output:a("输出","Output")},bt=(e,N)=>t("section",{className:"cm-stat-panel"},t("div",{className:"cm-stat-panel-head"},t("h3",null,e)),N),h=p?.totals,$=(e,N,at)=>t("div",{className:"cm-stat-metric",key:e},t("div",{className:"cm-stat-sub"},e),t("div",{className:"cm-stat-value"},N),t("div",{className:"cm-stat-sub"},at));return t("div",{className:"cm-stat"},t("style",null,Nt),t("header",{className:"cm-stat-head"},t("div",null,t("h2",null,a("计费统计","Cost statistics")),t("p",{className:"cm-stat-sub"},C.id?a("单对话 · ","Conversation · ")+(C.title===C.id&&p?.sessions[0]?.title||C.title):a("全部对话 · 从总额查看每一笔调用","All conversations · from totals to individual calls"))),B(a("刷新","Refresh"),()=>Z(e=>e+1))),C.id?B(a("← 全部对话","← All conversations"),()=>gt({id:"",title:""})):null,t("div",{className:"cm-stat-controls"},t("div",{className:"cm-stat-periods"},...[["today",a("今天","Today")],["week",a("近 7 天","Last 7 days")],["month",a("近 30 天","Last 30 days")],["all",a("全部记录","All retained")],["custom",a("自定义","Custom")]].map(([e,N])=>B(N,()=>ut(e),{key:e,"aria-pressed":i===e})))),i==="custom"?t("div",{className:"cm-stat-controls"},...["from","to"].map(e=>t("label",{key:e},e==="from"?a("开始日期","From"):a("结束日期","To"),t("input",{type:"date",value:e==="from"?u:v,onChange:N=>{W({from:u,to:v,[e]:N.target.value}),T(0)}})))):null,t("div",{className:"cm-stat-controls"},tt(a("计费口径","Cost basis"),x,Y,[["api",a("API 费用","API cost")],["plan",a("Plan 等值","Plan equivalent")],["total",a("API + Plan 等值","API + Plan equivalent")]]),tt(a("提供商","Provider"),f,e=>{M(e),j("")},[["",a("全部提供商","All providers")],...[...new Set([f,...p?.providers??[]])].filter(Boolean).map(e=>[e,e])]),tt(a("模型","Model"),R,j,[["",a("全部模型","All models")],...[...new Set([R,...p?.modelOptions??[]])].filter(Boolean).map(e=>[e,e])])),t("p",{className:"cm-stat-sub"},a("按宿主时区归日:","Days use the host timezone: ")+(s.meta.timezone||"UTC")+" · "+a("API 金额是按已记录用量和配置单价计算的估算;Plan 为订阅用量的 API 等值,不是订阅账单。外部导入用量在原概览中单列。","API amounts estimate recorded usage at configured rates; Plan is API-equivalent usage, not the subscription invoice. External usage remains separate in Overview.")),D.loading?t("p",{className:"cm-stat-empty",role:"status"},a("加载统计…","Loading statistics…")):D.error?t("p",{className:"cm-stat-error",role:"alert"},D.error):p?t(Q,null,t("p",{className:"cm-stat-sub",style:{marginTop:8}},p.from+" – "+p.to+(p.retainedFrom?" · "+a("账本保留范围 ","Retained ledger ")+p.retainedFrom+" – "+p.retainedTo:"")),t("div",{className:"cm-stat-metrics"},$(a("API 费用","API cost"),I(h.apiCost),a("已入账估算","Recorded estimate")),$(a("Plan 等值费用","Plan equivalent"),I(Math.max(0,h.cost-h.apiCost)),a("不代表实际扣款","Not an actual debit")),$(a("调用次数","Calls"),h.calls.toLocaleString(),p.sessionCount+a(" 个对话 · 平均 "," conversations · average ")+I(h.calls?S(h,x)/h.calls:0)),$(a("缓存命中率","Cache hit rate"),z(h.cacheRead,h.input+h.cacheRead+h.cacheWrite),m(et(h))+" Tokens")),C.id?t(mt,{key:JSON.stringify([C.id,u,v,f,R,x]),api:l,query:{...g,from:p.from,to:p.to,offset:0},revision:L,money:I,formatTokens:m,text:a}):null,p.models.some(e=>!e.priced)?t("p",{className:"cm-stat-note"},a("部分模型当前未配置价格,金额可能不完整;未定价不等于免费。","Some models have no configured price. Amounts may be incomplete; unpriced usage is not free.")):null,!h.calls&&!h.cost?t("p",{className:"cm-stat-empty"},a("所选范围没有已记录的用量。","No recorded usage in this range.")):null,t("section",{className:"cm-stat-panel"},t(rt,{rows:dt(p.days,x),money:I,text:a,label:a("费用趋势 · ","Cost over time · ")+wt,onSelect:e=>{W({from:e.from,to:e.to}),ut("custom")}}),t("p",{className:"cm-stat-sub",style:{marginTop:12}},a("点击柱形查看该日期范围。长区间按相邻日期合并,保留全部金额。","Select a bar to inspect its date range. Long ranges group adjacent days without dropping costs."))),t("div",{className:"cm-stat-grid"},bt(a("模型费用排行","Cost by model"),t(Q,null,t("ol",{className:"cm-stat-rank"},(U?p.models:p.models.slice(0,8)).map(e=>t("li",{key:e.key},t("button",{type:"button",onClick:()=>{M(e.provider),j(e.model),T(0)},title:e.key},t("div",{className:"cm-stat-rankline"},t("span",null,e.model),t("span",{className:"cm-stat-number"},I(S(e,x))+(e.priced?"":" + ?"))),t("div",{className:"cm-stat-sub"},e.provider+" · "+e.calls+a(" 次 · "," calls · ")+z(S(e,x),S(h,x))),t("div",{className:"cm-stat-track"},t("span",{style:{width:z(S(e,x),S(h,x))}}))))),p.models.length>8?B(U?a("收起","Show less"):a("显示全部模型","Show all models"),()=>_(e=>!e)):null,p.unmodeledCost>1e-8?t("p",{className:"cm-stat-note"},a("历史未分配到模型的费用:","Historical cost without a model: ")+I(p.unmodeledCost)):null)),bt(a("Token 构成","Token composition"),t(Q,null,t("ol",{className:"cm-stat-rank"},Object.entries(St).map(([e,N])=>t("li",{key:e},t("div",{className:"cm-stat-rankline"},t("span",null,N),t("span",{className:"cm-stat-number"},m(h[e]))),t("div",{className:"cm-stat-track"},t("span",{style:{width:z(h[e],et(h)),background:e.startsWith("cache")?"var(--cm-plan)":void 0}}))))),t("p",{className:"cm-stat-note"},a("这里展示 Token 数量占比,不是费用占比。缓存写入计入输入分母,不算缓存命中。","These are token shares, not cost shares. Cache writes count as input, not cache hits.")),t("p",{className:"cm-stat-sub"},a("单独上报的推理 Token:","Reported reasoning tokens: ")+m(h.reasoning))))),C.id?null:t("section",{className:"cm-stat-panel"},t("div",{className:"cm-stat-panel-head"},t("h3",null,a("对话费用排行","Cost by conversation")),t("span",{className:"cm-stat-sub"},a("点击对话查看明细 · 子代理分别计入,不重复相加","Select a conversation for details · each agent counted once"))),t("div",{className:"cm-stat-scroll"},t("table",{className:"cm-stat-table"},t("thead",null,t("tr",null,...[a("对话","Conversation"),a("调用","Calls"),"API",a("Plan 等值","Plan equivalent"),a("缓存命中","Cache hits")].map(e=>t("th",{key:e},e)))),t("tbody",null,p.sessions.map(e=>t("tr",{key:e.id},t("td",null,t("button",{type:"button",title:e.id,onClick:()=>gt({id:e.id,title:e.title})},e.title)),t("td",null,e.calls.toLocaleString()),t("td",null,I(e.apiCost)),t("td",null,I(Math.max(0,e.cost-e.apiCost))),t("td",null,z(e.cacheRead,e.input+e.cacheRead+e.cacheWrite))))))),p.unassignedCost>1e-8?t("p",{className:"cm-stat-note"},a("未关联对话的费用:","Cost not linked to a conversation: ")+I(p.unassignedCost)):null,t(X,{offset:F,count:p.sessionCount,size:25,onChange:T,text:a})))):null)}async function Ct(s){const l=await s.get("remote").$mount(ot);s.effect(()=>()=>l(),"cost-meter: statistics contribution");const o=s.get("remote.costMeter"),d=Object.fromEntries(["getBillingStatistics","getSessionBilling"].map(m=>[m,async c=>{const a=await o[m](lt(c));if(!a?.ok)throw new Error(a?.error?.message||"Statistics request failed");return(m==="getBillingStatistics"?V:G)(a.value)}]));return m=>t(pt,{...m,api:d})}return{mount:Ct,Statistics:pt,SessionDetail:mt,CONTRIBUTION:ot,parseStatistics:V,parseDetail:G,dailyChartRows:dt}}});
23
+ `;function nt(s,r,i,m){const[c,l]=y({value:null,error:"",loading:!0}),e=JSON.stringify(i);return Nt(()=>{let d=!0;return l({value:null,error:"",loading:!0}),s[r](JSON.parse(e)).then(x=>{d&&l({value:x,error:"",loading:!1})},x=>{d&&l({value:null,error:String(x.message??x),loading:!1})}),()=>{d=!1}},[s,r,e,m]),c}const R=(s,r,i={})=>t("button",{type:"button",className:"cm-stat-btn",onClick:r,...i},s);function J({offset:s,count:r,size:i,onChange:m,text:c}){return t("div",{className:"cm-stat-page"},t("span",{className:"cm-stat-sub"},r?`${s+1}–${Math.min(s+i,r)} / ${r}`:"0"),R(c("上一页","Previous"),()=>m(Math.max(0,s-i)),{disabled:!s}),R(c("下一页","Next"),()=>m(s+i),{disabled:s+i>=r}))}function gt({rows:s,money:r,text:i,onSelect:m,label:c}){const l=Math.max(0,...s.map(d=>d.value)),e=s.reduce((d,x)=>d+x.value,0);return t("div",null,t("div",{className:"cm-stat-panel-head"},t("h3",null,c),t("span",{className:"cm-stat-sub"},i("最高 ","Peak ")+r(l)+" · "+i("合计 ","Total ")+r(e))),t("div",{className:"cm-stat-chart",role:"group","aria-label":c},s.map((d,x)=>t("button",{key:x,type:"button",className:"cm-stat-bar",title:d.label+" · "+r(d.value),"aria-label":d.label+" · "+r(d.value),onClick:()=>m(d)},t("span",{style:{height:l?Math.max(1,d.value/l*100)+"%":"0%",opacity:d.value?void 0:.2}})))),t("div",{className:"cm-stat-axis"},t("span",null,s[0]?.label),t("span",null,s.at(-1)?.label)))}function ht(s,r){const i=Math.max(1,Math.ceil(s.length/60)),m=[];for(let c=0;c<s.length;c+=i){const l=s.slice(c,c+i),e=l[0].date,d=l.at(-1).date;m.push({from:e,to:d,label:e===d?e:e+" – "+d,value:l.reduce((x,w)=>x+T(w,r),0)})}return m}function H({title:s,rows:r,total:i,format:m,text:c,columns:l=!1}){return t("section",{"aria-label":s},t("h3",null,s),i<=0?t("p",{className:"cm-stat-sub"},c("暂无可计算的占比","No measurable share yet")):t("ol",{className:"cm-stat-share-list"+(l?" cm-stat-step-list":"")},r.map((e,d)=>t("li",{key:d,"data-other":e.other||void 0},t("div",{className:"cm-stat-share-head"},t("span",null,e.label),t("span",{className:"cm-stat-share-value"},m(e.value)+(e.unpriced?" + ?":"")+" · "+j(e.value,i))),t("div",{className:"cm-stat-share-track","aria-hidden":!0},t("span",{className:"cm-stat-share-fill",style:{width:Math.min(100,100*e.value/i)+"%"}}))))))}function bt({api:s,sessionId:r,turn:i,revision:m,text:c}){const[l,e]=y(0),[d,x]=y(0),w=nt(s,"getTurnInspection",{sessionId:r,turn:i,offset:l},m+":"+d);if(w.loading)return t("p",{className:"cm-stat-empty",role:"status"},c("读取这一轮的输入和工具调用…","Loading this turn’s input and tools…"));if(w.error)return t("div",{className:"cm-stat-inspection"},t("p",{className:"cm-stat-error",role:"alert"},w.error),R(c("重试","Retry"),()=>x(h=>h+1)));const C=w.value;return C.found?t("div",{className:"cm-stat-inspection"},t("h4",null,c("本轮用户输入","User input for this turn")),C.input?t("pre",{className:"cm-stat-pre"},C.input):t("p",{className:"cm-stat-sub"},c("日志未记录本轮用户输入。","No user input is recorded for this turn.")),C.inputTruncated?t("p",{className:"cm-stat-truncated"},c("输入过长,此处显示前 16,000 个字符。","Showing the first 16,000 characters of this input.")):null,t("h4",null,c("工具调用","Tool calls")+" · "+C.totalTools),C.totalTools?null:t("p",{className:"cm-stat-sub"},c("这一轮没有工具调用记录。","No tool calls are recorded in this turn.")),C.tools.map(h=>t("details",{key:h.seq,className:"cm-stat-tool"},t("summary",null,t("span",null,(h.step==null?"":c("步骤 ","Step ")+h.step+" · ")+h.name),t("span",{className:"cm-stat-sub"},{complete:c("已完成","Completed"),error:c("调用失败","Failed"),pending:c("未记录结果","No recorded result")}[h.status])),t("h4",{style:{marginTop:12}},c("调用参数","Arguments")),t("pre",{className:"cm-stat-pre"},h.arguments),t("h4",null,c("返回结果","Result")),t("pre",{className:"cm-stat-pre"},h.result||c("没有可显示的文本结果","No text result is available")),h.truncated?t("p",{className:"cm-stat-truncated"},c("长内容显示前 16,000 个字符;完整内容保留在会话轨迹中。","Long content is limited to 16,000 characters here; the full content remains in the conversation trajectory.")):null)),t(J,{offset:l,count:C.totalTools,size:20,onChange:e,text:c}),t("p",{className:"cm-stat-sub"},c("这里显示整轮原始记录,不受上方模型或计费筛选影响。附件仅显示类型。","Shows the whole turn, independent of model and cost filters above. Attachments are represented by type."))):t("p",{className:"cm-stat-note"},c("这一轮的原始日志不可用。","Original records for this turn are unavailable."))}function ft({api:s,query:r,revision:i,money:m,formatTokens:c,text:l}){const[e,d]=y(0),[x,w]=y(-1),[C,h]=y(0),[_,E]=y(null),[P,F]=y("cost"),A=nt(s,"getSessionBilling",{...r,offset:e,turnOffset:C},i),u=A.value,O=r.basis,L={input:l("输入","Input"),output:l("输出","Output"),cacheRead:l("缓存读取","Cache read"),cacheWrite:l("缓存写入","Cache write"),reasoning:l("推理","Reasoning")},M={model:l("模型调用","Model call"),compaction:l("上下文压缩","Compaction"),search:l("原生搜索","Native search")},W=a=>a==null?l("未标明轮次","Turn not recorded"):l("轮次 ","Turn ")+a;if(A.loading)return t("p",{className:"cm-stat-empty",role:"status"},l("正在读取本对话的用量明细…","Loading this conversation’s usage records…"));if(A.error)return t("p",{className:"cm-stat-error",role:"alert"},A.error);if(!u.found)return t("p",{className:"cm-stat-note"},l("没有可用的调用日志。上方账本统计仍然有效;明细不会按零费用处理。","Call logs are unavailable. The ledger totals above remain valid; missing details do not mean zero cost."));const lt=a=>O==="total"||(O==="plan"?a.plan:!a.plan),$=Object.keys(L).map(a=>{const o=u.rows.filter(b=>b.bucket===a&&lt(b));return{bucket:a,tokens:o.reduce((b,f)=>b+f.tokens,0),cost:o.reduce((b,f)=>b+f.cost,0),unpriced:o.some(b=>!b.priced)}}),B=T(u,O),z=T(u.recorded,O),U=Math.abs(B-z)>Math.max(1e-8,Math.abs(z)*1e-8)||u.totalCalls!==u.recorded.calls,Q=(a,o)=>t("p",{key:o,className:"cm-stat-sub"},L[a.bucket]+": "+a.tokens.toLocaleString()+" × "+(a.priced?m(a.rate):"?")+" / 1,000,000 = "+(a.priced?m(a.cost):"?")),K=(a,o,b)=>t("div",{className:"cm-stat-scroll",style:{marginTop:20}},t("h3",null,a),t("table",{className:"cm-stat-table"},t("thead",null,t("tr",null,...[a,l("调用次数","Calls"),l("输入 / 缓存 / 输出 Tokens","Input / cache / output tokens"),"API",l("Plan 等值","Plan equivalent")].map(f=>t("th",{key:f},f)))),t("tbody",null,o.map((f,V)=>t("tr",{key:V},t("td",null,b(f)),t("td",null,f.calls),t("td",null,[f.input,f.cacheRead+f.cacheWrite,f.output].map(c).join(" / ")),t("td",null,m(f.apiCost)+(f.unpriced?" + ?":"")),t("td",null,m(Math.max(0,f.cost-f.apiCost))+(f.unpriced?" + ?":"")))))));return t("section",{className:"cm-stat-panel"},t("div",{className:"cm-stat-panel-head"},t("h3",null,l("单对话费用明细","Conversation cost details")),t("span",{className:"cm-stat-sub"},l("只统计本对话自身的调用","Own calls only; child conversations are separate"))),t("details",{className:"cm-stat-help"},t("summary",null,l("统计口径","How these amounts are calculated")),t("p",null,l("明细按日志中的调用时间、用量和当前配置的历史价格规则计算。上方汇总采用已入账金额;调整价格后两者可能不同。","Details use logged usage and call times with the currently configured historical price rules. The summary above uses recorded ledger amounts; changing prices can produce a difference."))),U?t("p",{className:"cm-stat-note"},l("账本与可用明细不同:账本 ","Ledger and available details differ: ledger ")+m(z)+" / "+u.recorded.calls+l(" 次;明细 "," calls; details ")+m(B)+" / "+u.totalCalls+l(" 次。"," calls.")):null,t("div",{className:"cm-stat-shares"},t(H,{title:l("费用构成","Cost composition"),rows:$.map(a=>({label:L[a.bucket],value:a.cost,unpriced:a.unpriced})),total:B,format:m,text:l}),t(H,{title:l("调用类型占比 · 次数","Call type share · count"),rows:u.kinds.map(a=>({label:M[a.kind]??a.kind,value:a.calls})),total:u.totalCalls,format:a=>a.toLocaleString()+l(" 次"," calls"),text:l})),t("section",{className:"cm-stat-breakdown"},t("div",{className:"cm-stat-panel-head"},t("h3",null,l("各步骤占比","Share by step")),t("div",{className:"cm-stat-periods"},...[["cost",l("费用","Cost")],["calls",l("调用次数","Calls")]].map(([a,o])=>R(o,()=>F(a),{key:a,"aria-pressed":P===a})))),t(H,{title:P==="cost"?l("费用占比 · 全部调用","Cost share · all calls"):l("调用次数占比 · 全部调用","Call count share · all calls"),rows:(u.stepShares?.[P]??[]).map(a=>({label:a.other?l("其余步骤合计","All other steps"):W(a.turn)+(a.step==null?"":l(" · 步骤 "," · step ")+a.step),value:P==="cost"?T(a,O):a.calls,unpriced:P==="cost"&&a.unpriced,other:a.other})),total:P==="cost"?B:u.totalCalls,format:P==="cost"?m:a=>a.toLocaleString(),text:l,columns:!0}),t("p",{className:"cm-stat-sub",style:{marginTop:12}},l("分母包含所选范围的全部调用,跨页合计。显示前 12 项,其余合并;无步骤编号的调用单列。工具本身不额外算作模型调用。","Shares use all calls in the selected range, across pages. Top 12 entries are shown; the rest are combined. Missing step IDs remain explicit. Tools are not counted as extra model calls."))),t("details",{className:"cm-stat-help"},t("summary",null,l("单价与 Token 明细","Rates and token details")),t("div",{className:"cm-stat-scroll"},t("table",{className:"cm-stat-table"},t("thead",null,t("tr",null,...[l("费用构成","Cost component"),"Tokens",l("费用","Cost"),l("费用占比","Cost share")].map(a=>t("th",{key:a},a)))),t("tbody",null,$.map(a=>t("tr",{key:a.bucket},t("td",null,L[a.bucket]),t("td",null,a.tokens.toLocaleString()),t("td",null,m(a.cost)+(a.unpriced?" + ?":"")),t("td",null,j(a.cost,B))))))),t("p",{className:"cm-stat-sub",style:{marginTop:10}},l("推理 Token 可能包含在输出中;单价为 0 时不另收推理费。未定价用量显示 ?。","Reasoning tokens may overlap output; a zero reasoning rate adds no separate charge. Unpriced usage is marked ?.")),K(l("按调用类型统计","Cost by call type"),u.kinds,a=>M[a.kind]??a.kind)),t("section",{className:"cm-stat-turns"},t("div",{className:"cm-stat-panel-head"},t("h3",null,l("按轮次统计","Cost by turn")),t("span",{className:"cm-stat-sub"},l("展开查看输入和工具调用","Expand to inspect input and tool calls"))),u.turns.map(a=>t("div",{key:a.turn??"unknown",className:"cm-stat-turn"},t("button",{type:"button",className:"cm-stat-turn-toggle",disabled:a.turn==null,"aria-expanded":a.turn!=null&&_===a.turn,onClick:()=>E(o=>o===a.turn?null:a.turn)},t("span",null,(a.turn==null?"":_===a.turn?"⌄ ":"› ")+W(a.turn)),t("span",null,a.calls+l(" 次调用"," calls")+" · "+c(Z(a))+" tok · "+m(T(a,O))+(a.unpriced?" + ?":""))),a.turn!=null&&_===a.turn?t(bt,{key:a.turn,api:s,sessionId:r.sessionId,turn:a.turn,revision:i,text:l}):null))),t("p",{className:"cm-stat-sub"},l("轮次沿用日志编号;未记录轮次的调用单列。每轮包含其全部调用,跨调用分页也不会拆分。","Turn numbers come from the log; calls without one are listed separately. Each turn includes all its calls, across call pages.")),t(J,{offset:C,count:u.totalTurns,size:25,onChange:a=>{h(a),E(null)},text:l}),t("div",{style:{marginTop:24}},t(gt,{rows:u.calls.map((a,o)=>({label:"#"+(e+o+1),index:o,value:T(a,O)})),money:m,text:l,label:l("逐次调用费用 · 当前页","Cost per call · current page"),onSelect:a=>{w(a.index),document.getElementById("cm-stat-call-"+a.index)?.scrollIntoView({block:"nearest",behavior:"smooth"})}})),u.calls.map((a,o)=>t("details",{key:o,id:"cm-stat-call-"+o,className:"cm-stat-call",open:x===o,onToggle:b=>{!b.currentTarget.open&&x===o&&w(-1)}},t("summary",null,"#"+(e+o+1)+" · "+W(a.turn)+(a.step==null?"":l(" / 步骤 "," / step ")+a.step)+" · "+(M[a.kind]??a.kind)+" · "+a.provider+" / "+a.model+" · "+(a.plan?"Plan ":"API ")+(a.priced?m(a.cost):"?")+(a.longContext?l(" · 长上下文价"," · long-context rate"):"")),t("p",{className:"cm-stat-sub"},new Date(a.atMs).toLocaleString()),a.rows.map(Q))),t(J,{offset:e,count:u.totalCalls,size:50,onChange:a=>{d(a),w(-1)},text:l}))}function vt({state:s,api:r,sessionId:i="",formatMoneyUsd:m,formatTokens:c}){const l=s.config.locale==="en"||s.config.locale==="auto"&&!/^zh\b/i.test(navigator.language),e=(n,S)=>l?S:n,[d,x]=y(i?"all":"week"),[w,C]=y(null),[h,_]=y({id:i,title:i}),[E,P]=y(""),[F,A]=y(""),[u,O]=y(i?"total":"api"),[L,M]=y(0),[W,lt]=y(0),[$,B]=y(!1),z=s.meta.dayKey||new Date().toISOString().slice(0,10),U=d==="all"?"":d==="custom"?w?.from||z:wt(z,d==="week"?-6:d==="month"?-29:0),Q=d==="custom"&&w?.to||z,K={from:U,to:Q,provider:E,model:F,basis:u,sessionId:h.id,offset:L},a=nt(r,"getBillingStatistics",K,W+":"+s.meta.now),o=a.value,b=n=>m(n,{...s.config,decimals:Math.max(n!==0&&Math.abs(n)<.01?8:4,s.config.decimals??2)}),f=n=>{x(n),M(0)},V=n=>{_(n),M(0),P(""),A("")},Pt=u==="total"?e("API + Plan 等值","API + Plan equivalent"):u==="plan"?e("Plan 等值","Plan equivalent"):e("API 费用","API cost"),rt=(n,S,ct,At)=>t("label",null,n,t("select",{value:S,onChange:X=>{ct(X.target.value),M(0)}},At.map(([X,Ot])=>t("option",{key:X,value:X},Ot)))),Mt={input:e("未缓存输入","Uncached input"),cacheRead:e("缓存读取","Cache read"),cacheWrite:e("缓存写入","Cache write"),output:e("输出","Output")},yt=(n,S)=>t("section",{className:"cm-stat-panel"},t("div",{className:"cm-stat-panel-head"},t("h3",null,n)),S),v=o?.totals,G=(n,S,ct)=>t("div",{className:"cm-stat-metric",key:n},t("div",{className:"cm-stat-sub"},n),t("div",{className:"cm-stat-value"},S),t("div",{className:"cm-stat-sub"},ct));return t("div",{className:"cm-stat"},t("style",null,Tt),t("header",{className:"cm-stat-head"},t("div",null,t("h2",null,e("计费统计","Cost statistics")),t("p",{className:"cm-stat-sub"},h.id?e("单对话 · ","Conversation · ")+(h.title===h.id&&o?.sessions[0]?.title||h.title):e("全部对话 · 从总额查看每一笔调用","All conversations · from totals to individual calls"))),R(e("刷新","Refresh"),()=>lt(n=>n+1))),h.id?R(e("← 全部对话","← All conversations"),()=>V({id:"",title:""})):null,t("div",{className:"cm-stat-controls"},t("div",{className:"cm-stat-periods"},...[["today",e("今天","Today")],["week",e("近 7 天","Last 7 days")],["month",e("近 30 天","Last 30 days")],["all",e("全部记录","All retained")],["custom",e("自定义","Custom")]].map(([n,S])=>R(S,()=>f(n),{key:n,"aria-pressed":d===n})))),d==="custom"?t("div",{className:"cm-stat-controls"},...["from","to"].map(n=>t("label",{key:n},n==="from"?e("开始日期","From"):e("结束日期","To"),t("input",{type:"date",value:n==="from"?U:Q,onChange:S=>{C({from:U,to:Q,[n]:S.target.value}),M(0)}})))):null,t("div",{className:"cm-stat-controls"},rt(e("计费口径","Cost basis"),u,O,[["api",e("API 费用","API cost")],["plan",e("Plan 等值","Plan equivalent")],["total",e("API + Plan 等值","API + Plan equivalent")]]),rt(e("提供商","Provider"),E,n=>{P(n),A("")},[["",e("全部提供商","All providers")],...[...new Set([E,...o?.providers??[]])].filter(Boolean).map(n=>[n,n])]),rt(e("模型","Model"),F,A,[["",e("全部模型","All models")],...[...new Set([F,...o?.modelOptions??[]])].filter(Boolean).map(n=>[n,n])])),t("details",{className:"cm-stat-help"},t("summary",null,e("计费说明","About billing")),t("p",null,e("按宿主时区归日:","Days use the host timezone: ")+(s.meta.timezone||"UTC")+" · "+e("API 金额是按已记录用量和配置单价计算的估算;Plan 为订阅用量的 API 等值,不是订阅账单。外部导入用量在原概览中单列。","API amounts estimate recorded usage at configured rates; Plan is API-equivalent usage, not the subscription invoice. External usage remains separate in Overview."))),a.loading?t("p",{className:"cm-stat-empty",role:"status"},e("加载统计…","Loading statistics…")):a.error?t("p",{className:"cm-stat-error",role:"alert"},a.error):o?t(Y,null,t("p",{className:"cm-stat-sub",style:{marginTop:8}},o.from+" – "+o.to+(o.retainedFrom?" · "+e("账本保留范围 ","Retained ledger ")+o.retainedFrom+" – "+o.retainedTo:"")),t("div",{className:"cm-stat-metrics"},G(e("API 费用","API cost"),b(v.apiCost),e("已入账估算","Recorded estimate")),G(e("Plan 等值费用","Plan equivalent"),b(Math.max(0,v.cost-v.apiCost)),e("不代表实际扣款","Not an actual debit")),G(e("调用次数","Calls"),v.calls.toLocaleString(),o.sessionCount+e(" 个对话 · 平均 "," conversations · average ")+b(v.calls?T(v,u)/v.calls:0)),G(e("缓存命中率","Cache hit rate"),j(v.cacheRead,v.input+v.cacheRead+v.cacheWrite),c(Z(v))+" Tokens")),h.id?t(ft,{key:JSON.stringify([h.id,U,Q,E,F,u]),api:r,query:{...K,from:o.from,to:o.to,offset:0},revision:W,money:b,formatTokens:c,text:e}):null,o.models.some(n=>!n.priced)?t("p",{className:"cm-stat-note"},e("部分模型当前未配置价格,金额可能不完整;未定价不等于免费。","Some models have no configured price. Amounts may be incomplete; unpriced usage is not free.")):null,!v.calls&&!v.cost?t("p",{className:"cm-stat-empty"},e("所选范围没有已记录的用量。","No recorded usage in this range.")):null,t("section",{className:"cm-stat-panel"},t(gt,{rows:ht(o.days,u),money:b,text:e,label:e("费用趋势 · ","Cost over time · ")+Pt,onSelect:n=>{C({from:n.from,to:n.to}),f("custom")}}),t("p",{className:"cm-stat-sub",style:{marginTop:12}},e("点击柱形查看该日期范围。长区间按相邻日期合并,保留全部金额。","Select a bar to inspect its date range. Long ranges group adjacent days without dropping costs."))),t("div",{className:"cm-stat-grid"},yt(e("模型费用排行","Cost by model"),t(Y,null,t("ol",{className:"cm-stat-rank"},($?o.models:o.models.slice(0,8)).map(n=>t("li",{key:n.key},t("button",{type:"button",onClick:()=>{P(n.provider),A(n.model),M(0)},title:n.key},t("div",{className:"cm-stat-rankline"},t("span",null,n.model),t("span",{className:"cm-stat-number"},b(T(n,u))+(n.priced?"":" + ?"))),t("div",{className:"cm-stat-sub"},n.provider+" · "+n.calls+e(" 次 · "," calls · ")+j(T(n,u),T(v,u))),t("div",{className:"cm-stat-track"},t("span",{style:{width:j(T(n,u),T(v,u))}}))))),o.models.length>8?R($?e("收起","Show less"):e("显示全部模型","Show all models"),()=>B(n=>!n)):null,o.unmodeledCost>1e-8?t("p",{className:"cm-stat-note"},e("历史未分配到模型的费用:","Historical cost without a model: ")+b(o.unmodeledCost)):null)),yt(e("Token 构成","Token composition"),t(Y,null,t("ol",{className:"cm-stat-rank"},Object.entries(Mt).map(([n,S])=>t("li",{key:n},t("div",{className:"cm-stat-rankline"},t("span",null,S),t("span",{className:"cm-stat-number"},c(v[n]))),t("div",{className:"cm-stat-track"},t("span",{style:{width:j(v[n],Z(v)),background:n.startsWith("cache")?"var(--cm-plan)":void 0}}))))),t("p",{className:"cm-stat-note"},e("这里展示 Token 数量占比,不是费用占比。缓存写入计入输入分母,不算缓存命中。","These are token shares, not cost shares. Cache writes count as input, not cache hits.")),t("p",{className:"cm-stat-sub"},e("单独上报的推理 Token:","Reported reasoning tokens: ")+c(v.reasoning))))),h.id?null:t("section",{className:"cm-stat-panel"},t("div",{className:"cm-stat-panel-head"},t("h3",null,e("对话费用排行","Cost by conversation")),t("span",{className:"cm-stat-sub"},e("点击对话查看明细 · 子代理分别计入,不重复相加","Select a conversation for details · each agent counted once"))),t("div",{className:"cm-stat-scroll"},t("table",{className:"cm-stat-table"},t("thead",null,t("tr",null,...[e("对话","Conversation"),e("调用","Calls"),"API",e("Plan 等值","Plan equivalent"),e("缓存命中","Cache hits")].map(n=>t("th",{key:n},n)))),t("tbody",null,o.sessions.map(n=>t("tr",{key:n.id},t("td",null,t("button",{type:"button",title:n.id,onClick:()=>V({id:n.id,title:n.title})},n.title)),t("td",null,n.calls.toLocaleString()),t("td",null,b(n.apiCost)),t("td",null,b(Math.max(0,n.cost-n.apiCost))),t("td",null,j(n.cacheRead,n.input+n.cacheRead+n.cacheWrite))))))),o.unassignedCost>1e-8?t("p",{className:"cm-stat-note"},e("未关联对话的费用:","Cost not linked to a conversation: ")+b(o.unassignedCost)):null,t(J,{offset:L,count:o.sessionCount,size:25,onChange:M,text:e})))):null)}async function It(s){const r=await s.get("remote").$mount(ut);s.effect(()=>()=>r(),"cost-meter: statistics contribution");const i=s.get("remote.costMeter"),m=Object.fromEntries(["getBillingStatistics","getSessionBilling","getTurnInspection"].map(c=>[c,async l=>{const e=await i[c]((c==="getTurnInspection"?mt:dt)(l));if(!e?.ok)throw new Error(e?.error?.message||"Statistics request failed");return{getBillingStatistics:at,getSessionBilling:et,getTurnInspection:st}[c](e.value)}]));return c=>t(vt,{...c,api:m})}return{mount:It,Statistics:vt,SessionDetail:ft,TurnInspection:bt,ShareChart:H,CONTRIBUTION:ut,parseStatistics:at,parseDetail:et,parseInspection:st,dailyChartRows:ht}}});
package/lib/index.js CHANGED
@@ -38,6 +38,7 @@ import { queryQwenCli } from './qwen-cli.js'
38
38
  import { queryBailianCli } from './bailian-cli.js'
39
39
  import { getSessionCost as readSessionCost } from './session-tree.js'
40
40
  import { getTurnCost, getSessionBilling } from './turn-cost.js'
41
+ import { getTurnInspection } from './turn-inspection.js'
41
42
  import { billingStatistics } from './billing-statistics.js'
42
43
 
43
44
  export const name = 'cost-meter'
@@ -2398,6 +2399,7 @@ function createService(ctx, ledger, openRouterPrices) {
2398
2399
  },
2399
2400
  async getBillingStatistics(query) { return billingStatistics(ledger, query) },
2400
2401
  async getSessionBilling(query) { return getSessionBilling(ledger, ctx, query) },
2402
+ async getTurnInspection(query) { return getTurnInspection(ctx, query) },
2401
2403
  async getTopSessions(limit, sort = 'cost', dir = 'desc') {
2402
2404
  const n = Math.max(1, Math.min(500, Math.floor(Number(limit)) || 100))
2403
2405
  const sortKey = sort === 'time' || sort === 'recent' ? sort : 'cost'
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * DeepSeek 原生搜索不经过 llm/stream,且宿主会丢弃 Messages 响应的 usage。
3
- * 只在 web.search 的异步作用域内订阅官方 Messages 响应诊断;不替换 fetch、
3
+ * 在搜索作用域内观察官方 Messages 响应诊断及 fetch 的已解析 JSON。
4
4
  * 不读取请求 headers/body,也不改变搜索返回值、取消或错误语义。
5
5
  */
6
6
  import { AsyncLocalStorage } from 'node:async_hooks'
@@ -10,6 +10,7 @@ import { readFileSync, writeFileSync, renameSync, mkdirSync, statSync } from 'no
10
10
  import { dirname } from 'node:path'
11
11
  import { gunzipSync, inflateSync, brotliDecompressSync } from 'node:zlib'
12
12
  import { recordNativeSearchUsage } from './native-search-history.js'
13
+ import { observeSearchFetch } from './native-search-fetch.js'
13
14
  export { NATIVE_SEARCH_USAGE_EVENT, isNativeSearchUsageEvent } from './native-search-events.js'
14
15
 
15
16
  const MAX_BODY_BYTES = 4 * 1024 * 1024
@@ -70,9 +71,10 @@ function parseBody(request) {
70
71
  * 可注入诊断通道/目标判定仅用于不付费的本地回归;生产固定使用官方端点。
71
72
  * 每个 HTTP request 独立计数,相同 token 的并发/重复搜索不会相互去重。
72
73
  */
73
- export function createNativeSearchBilling({ account, record = () => {}, uncovered = () => {}, diagnose = () => {}, now = Date.now, targetRequest = officialRequest, diagnostics = channel }) {
74
+ export function createNativeSearchBilling({ account, record = () => {}, uncovered = () => {}, diagnose = () => {}, now = Date.now, targetRequest = officialRequest, diagnostics = channel, fetchTarget = globalThis }) {
74
75
  const scope = new AsyncLocalStorage()
75
76
  const searchSignal = new AsyncLocalStorage()
77
+ const fetchScope = new AsyncLocalStorage()
76
78
  const requests = new WeakMap()
77
79
  const pending = new Set()
78
80
  const signals = new Set()
@@ -87,6 +89,7 @@ export function createNativeSearchBilling({ account, record = () => {}, uncovere
87
89
  subscriptions.push(() => source.unsubscribe(safe))
88
90
  }
89
91
  const miss = (item, reason) => {
92
+ item = item.attempt?.item ?? item
90
93
  if (item.coverageReported) return
91
94
  item.coverageReported = true
92
95
  try { uncovered({ startedAtMs: item.startedAtMs, reason }) } catch {}
@@ -115,16 +118,64 @@ export function createNativeSearchBilling({ account, record = () => {}, uncovere
115
118
  pending.delete(item)
116
119
  requests.delete(request)
117
120
  }
121
+ const commit = (item, parsed) => {
122
+ if (!active || item.attempt?.accounted) return
123
+ const event = { ...parsed, provider: 'deepseek-official', startedAtMs: item.startedAtMs, requestId: item.requestId }
124
+ if (item.attempt) item.attempt.accounted = true
125
+ try { account(event, item.session) } catch { miss(item, 'account-failed'); return }
126
+ seen('accounted')
127
+ if (item.session) {
128
+ try { record(item.session, event) } catch { miss(item, 'history-unavailable') }
129
+ } else miss(item, 'history-unavailable')
130
+ }
131
+ const currentSearch = () => {
132
+ const signal = searchSignal.getStore()
133
+ const current = scope.getStore()
134
+ return current && !current.closed ? current : signal?.pending && !signal.owner?.closed && now() - signal.at < 60_000 ? signal : null
135
+ }
136
+ const restoreFetch = observeSearchFetch({ target: fetchTarget, seen,
137
+ context: () => active ? currentSearch() : null,
138
+ run(attempt, operation) {
139
+ const signal = searchSignal.getStore()
140
+ if (signal) finishSignal(signal)
141
+ const item = { session: attempt.current.session, startedAtMs: now(), requestId: randomUUID(), attempt, done: false }
142
+ item.release = () => { clearTimeout(item.timer); pending.delete(item) }
143
+ item.timer = setTimeout(() => { item.done = true; if (!attempt.accounted) miss(item, 'response-incomplete'); item.release() }, 60_000)
144
+ item.timer.unref?.()
145
+ pending.add(item)
146
+ attempt.item = item
147
+ attempt.current.requests.push(item)
148
+ try { return fetchScope.run(attempt, operation) } catch (error) { item.done = true; item.release(); miss(item, 'response-incomplete'); throw error }
149
+ },
150
+ complete(attempt, response) {
151
+ if (attempt.item.done) return
152
+ attempt.item.done = true
153
+ attempt.item.release()
154
+ if (!active || attempt.accounted) return
155
+ seen('fetchResponse')
156
+ const parsed = nativeSearchUsage(response)
157
+ if (parsed) commit(attempt.item, parsed)
158
+ else miss(attempt.item, 'usage-unavailable')
159
+ },
160
+ ignored(attempt) { attempt.item.done = true; attempt.item.release() },
161
+ failed(attempt) { if (attempt.item.done) return; attempt.item.done = true; attempt.item.release(); if (active && !attempt.accounted) miss(attempt.item, 'response-incomplete') },
162
+ })
118
163
  subscribe('create', ({ request }) => {
164
+ seen('requestCreateAny')
165
+ if (request?.method !== 'POST') seen('requestMethodMismatch')
166
+ else if (String(request.origin) !== 'https://api.deepseek.com') seen('requestOriginMismatch')
167
+ else if (request.path !== '/anthropic/v1/messages') seen('requestPathMismatch')
119
168
  if (!targetRequest(request)) return
120
169
  seen('officialRequest')
121
170
  const signal = searchSignal.getStore()
122
171
  const scoped = scope.getStore()
123
- const current = scoped ?? (signal?.pending && now() - signal.at < 60_000 ? signal : null)
172
+ const current = currentSearch() ?? fetchScope.getStore()?.current
173
+ if (current?.closed) return
124
174
  if (!current) { seen('unscopedRequest'); return }
125
175
  seen('matchedRequest')
126
176
  if (signal) finishSignal(signal)
127
177
  const item = { session: current.session, startedAtMs: now(), requestId: randomUUID(), bytes: 0, chunks: [], encoding: '', status: 0, done: false }
178
+ item.attempt = fetchScope.getStore()
128
179
  requests.set(request, item)
129
180
  current.requests.push(item)
130
181
  pending.add(item)
@@ -172,13 +223,8 @@ export function createNativeSearchBilling({ account, record = () => {}, uncovere
172
223
  try {
173
224
  if (item.status < 200 || item.status >= 300) return
174
225
  const parsed = item.tooLarge || item.bytes === 0 ? null : parseBody(item)
175
- if (!parsed) { miss(item, 'usage-unavailable'); return }
176
- const event = { ...parsed, provider: 'deepseek-official', startedAtMs: item.startedAtMs, requestId: item.requestId }
177
- try { account(event, item.session) } catch { miss(item, 'account-failed'); return }
178
- seen('accounted')
179
- if (item.session) {
180
- try { record(item.session, event) } catch { miss(item, 'history-unavailable') }
181
- } else miss(item, 'history-unavailable')
226
+ if (!parsed) { if (!item.attempt) miss(item, 'usage-unavailable'); return }
227
+ commit(item, parsed)
182
228
  } catch {
183
229
  miss(item, 'usage-unavailable')
184
230
  } finally {
@@ -213,11 +259,12 @@ export function createNativeSearchBilling({ account, record = () => {}, uncovere
213
259
  }
214
260
  },
215
261
  async run(operation, session) {
216
- if (!active || scope.getStore()) return operation()
262
+ if (!active || scope.getStore() && !scope.getStore().closed) return operation()
217
263
  seen('searchScope')
218
264
  const current = { session, requests: [] }
219
265
  try { return await scope.run(current, operation) }
220
266
  finally {
267
+ current.closed = true
221
268
  // A host event without any matching request used to leave no evidence.
222
269
  // Only official search events imply a gap; other providers also use web.search.
223
270
  for (const signal of signals) if (signal.owner === current) {
@@ -227,7 +274,7 @@ export function createNativeSearchBilling({ account, record = () => {}, uncovere
227
274
  for (const item of current.requests) {
228
275
  if (!item.done) {
229
276
  item.done = true
230
- if (active) miss(item, 'response-incomplete')
277
+ if (active && !item.attempt?.accounted) miss(item, 'response-incomplete')
231
278
  item.release()
232
279
  }
233
280
  }
@@ -241,6 +288,8 @@ export function createNativeSearchBilling({ account, record = () => {}, uncovere
241
288
  pending.clear()
242
289
  scope.disable()
243
290
  searchSignal.disable()
291
+ fetchScope.disable()
292
+ restoreFetch()
244
293
  },
245
294
  }
246
295
  }
@@ -276,7 +325,7 @@ export function createSearchCoverage(path, now = Date.now) {
276
325
  schedule()
277
326
  return {
278
327
  observe(name) {
279
- if (!['searchScope', 'searchEvent', 'otherEndpoint', 'officialRequest', 'unscopedRequest', 'matchedRequest', 'responseHeaders', 'responseComplete', 'accounted', 'webObserved'].includes(name)) return
328
+ if (!['searchScope', 'searchEvent', 'otherEndpoint', 'officialRequest', 'unscopedRequest', 'matchedRequest', 'responseHeaders', 'responseComplete', 'accounted', 'webObserved', 'fetchRequest', 'fetchResponse', 'requestCreateAny', 'requestMethodMismatch', 'requestOriginMismatch', 'requestPathMismatch'].includes(name)) return
280
329
  increment(runtime.counters, name); schedule()
281
330
  },
282
331
  add({ startedAtMs, reason }) {
@@ -0,0 +1,35 @@
1
+ /** Observe the parsed response of a scoped official search, including non-Undici fetch transports. */
2
+ export function observeSearchFetch({ context, run, complete, failed, ignored, seen, target = globalThis }) {
3
+ const original = target.fetch
4
+ if (typeof original !== 'function' || Object.getOwnPropertyDescriptor(target, 'fetch')?.writable === false) return () => {}
5
+ const wrapped = function (...args) {
6
+ const [input, init] = args
7
+ let url, method
8
+ try {
9
+ url = typeof input === 'string' ? input : input instanceof URL ? input.href : input?.url
10
+ method = init?.method ?? input?.method ?? 'GET'
11
+ } catch { return Reflect.apply(original, this, args) }
12
+ const current = url === 'https://api.deepseek.com/anthropic/v1/messages' && String(method).toUpperCase() === 'POST' ? context() : null
13
+ if (!current) return Reflect.apply(original, this, args)
14
+ const attempt = { current, accounted: false }
15
+ seen('fetchRequest')
16
+ // Observe the same promise and parsed value; do not tee/consume streams, read
17
+ // credentials or queries, alter the dispatcher, or delay the caller's result.
18
+ const promise = run(attempt, () => Reflect.apply(original, this, args))
19
+ Promise.resolve(promise).then(response => {
20
+ if (!response?.ok) { ignored(attempt); return }
21
+ if (typeof response.json !== 'function') return
22
+ const own = Object.getOwnPropertyDescriptor(response, 'json'), json = response.json
23
+ if (own?.configurable === false || !Object.isExtensible(response)) return
24
+ Object.defineProperty(response, 'json', { configurable: true, writable: true, value: function (...jsonArgs) {
25
+ let result
26
+ try { result = Reflect.apply(json, this, jsonArgs) } catch (error) { failed(attempt); throw error }
27
+ Promise.resolve(result).then(value => complete(attempt, value), () => failed(attempt)).catch(() => {})
28
+ return result
29
+ } })
30
+ }, () => failed(attempt)).catch(() => {})
31
+ return promise
32
+ }
33
+ target.fetch = wrapped
34
+ return () => { if (target.fetch === wrapped) target.fetch = original }
35
+ }
package/lib/turn-cost.js CHANGED
@@ -6,6 +6,7 @@ import { nativeSearchRecordsFor } from './native-search-history.js'
6
6
  import { tierFor, usdFromCost } from './pricing.js'
7
7
  import { isNativeSearchUsageEvent } from './native-search-events.js'
8
8
  import { ledgerStatisticsQuery, matchesStats, selectedStats, emptyStats, STAT_FIELDS } from './billing-statistics.js'
9
+ import { localDayKey } from './store.js'
9
10
 
10
11
  const cache = new Map()
11
12
  const fields = { input: 'cacheMiss', output: 'output', cacheRead: 'cacheHit', cacheWrite: 'cacheWrite', reasoning: 'reasoning' }
@@ -94,9 +95,10 @@ export async function getSessionBilling(ledger, ctx, raw) {
94
95
  const detail = costFromSamples(selected, ledger.config)
95
96
  // Aggregate the complete selection before paging. Unknown round identities stay
96
97
  // explicit; timing alone cannot reliably assign a search or compaction to a turn.
97
- const kinds = new Map(), turns = new Map()
98
+ const kinds = new Map(), turns = new Map(), steps = new Map()
98
99
  for (const sample of selected) {
99
- for (const [map, key, identity] of [[kinds, sample.kind, { kind: sample.kind }], [turns, sample.turn, { turn: sample.turn }]]) {
100
+ for (const [map, key, identity] of [[kinds, sample.kind, { kind: sample.kind }], [turns, sample.turn, { turn: sample.turn }],
101
+ [steps, JSON.stringify([sample.turn, sample.step]), { turn: sample.turn ?? null, step: sample.step ?? null, other: false }]]) {
100
102
  const row = map.get(key) ?? { ...identity, ...emptyStats(), unpriced: false }
101
103
  for (const bucket of Object.keys(fields)) row[bucket] += sample.buckets[bucket]
102
104
  row.calls++; row.cost += sample.cost; row.apiCost += sample.apiCost
@@ -104,6 +106,19 @@ export async function getSessionBilling(ledger, ctx, raw) {
104
106
  map.set(key, row)
105
107
  }
106
108
  }
109
+ const share = metric => {
110
+ const value = row => metric === 'calls' ? row.calls : query.basis === 'plan' ? row.cost - row.apiCost : query.basis === 'api' ? row.apiCost : row.cost
111
+ const all = [...steps.values()].sort((a, b) => value(b) - value(a) || b.calls - a.calls)
112
+ if (all.length <= 12) return all
113
+ const rest = { turn: null, step: null, other: true, ...emptyStats(), unpriced: false }
114
+ for (const row of all.slice(12)) { for (const key of STAT_FIELDS) rest[key] += row[key]; rest.unpriced ||= row.unpriced }
115
+ return [...all.slice(0, 12), rest]
116
+ }
117
+ if (!query.provider && !query.model) for (const row of records) {
118
+ if (row.type !== 'turn/start' || !Number.isSafeInteger(row.data?.turn) || row.data.turn < 0 || !Number.isFinite(row.time)) continue
119
+ const date = localDayKey(row.time)
120
+ if (date >= query.from && date <= query.to && !turns.has(row.data.turn)) turns.set(row.data.turn, { turn: row.data.turn, ...emptyStats(), unpriced: false })
121
+ }
107
122
  const orderedTurns = [...turns.values()].sort((a, b) => (a.turn ?? Infinity) - (b.turn ?? Infinity))
108
123
  const recorded = emptyStats()
109
124
  for (const [date, day] of Object.entries(ledger.days)) {
@@ -112,6 +127,7 @@ export async function getSessionBilling(ledger, ctx, raw) {
112
127
  for (const key of STAT_FIELDS) recorded[key] += row[key]
113
128
  }
114
129
  // Limit transport size, not the totals or category breakdown. Every call is reachable by paging.
115
- return { ...detail, calls: detail.calls.slice(query.offset, query.offset + 50), totalCalls: detail.calls.length, offset: query.offset, recorded,
116
- kinds: [...kinds.values()], turns: orderedTurns.slice(query.turnOffset, query.turnOffset + 25), totalTurns: orderedTurns.length, turnOffset: query.turnOffset }
130
+ return { ...detail, found: detail.found || orderedTurns.length > 0, calls: detail.calls.slice(query.offset, query.offset + 50), totalCalls: detail.calls.length, offset: query.offset, recorded,
131
+ kinds: [...kinds.values()], turns: orderedTurns.slice(query.turnOffset, query.turnOffset + 25), totalTurns: orderedTurns.length, turnOffset: query.turnOffset,
132
+ stepShares: { cost: share('cost'), calls: share('calls') } }
117
133
  }
@@ -0,0 +1,63 @@
1
+ /** Read a single turn's user input and tools on demand. Never include system prompts or request headers. */
2
+ import { join } from 'node:path'
3
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
4
+ import { listSessionLogs, iterateSessionRecords } from './backfill.js'
5
+
6
+ const LIMIT = 16000
7
+ const identity = n => Number.isSafeInteger(n) && n >= 0
8
+ const clipped = value => { const text = typeof value === 'string' ? value : JSON.stringify(value ?? '', null, 2); return { text: text.slice(0, LIMIT), truncated: text.length > LIMIT } }
9
+ const contentText = content => typeof content === 'string' ? content : Array.isArray(content) ? content.map(block => {
10
+ if (block?.type === 'text') return String(block.text ?? '')
11
+ // Binary attachments and remote URLs are never fetched for this view.
12
+ return `[${String(block?.type ?? 'attachment')}]`
13
+ }).join('\n') : ''
14
+
15
+ export async function inspectTurn(records, turn, offset = 0) {
16
+ let active = null, found = false, input = '', inputTruncated = false, totalTools = 0, scanned = 0
17
+ const tools = [], selected = new Map(), inputIds = new Set()
18
+ for await (const event of records) {
19
+ if (++scanned > 500000) throw new Error('Session exceeds the turn inspection limit')
20
+ const data = event.data ?? {}
21
+ if (event.type === 'turn/start') { active = data.turn; if (active === turn) found = true }
22
+ const belongs = (identity(data.turn) ? data.turn : active) === turn
23
+ if (belongs) {
24
+ found = true
25
+ if (event.type === 'user/message' && (!event.surfaceOp || event.surfaceOp === 'append')) {
26
+ const id = data.id ?? event.seq
27
+ if (!inputIds.has(id)) {
28
+ inputIds.add(id)
29
+ const next = clipped(input + (input ? '\n\n' : '') + contentText(data.content))
30
+ input = next.text; inputTruncated ||= next.truncated
31
+ }
32
+ }
33
+ if (event.type === 'tool/call') {
34
+ const index = totalTools++
35
+ if (index >= offset && index < offset + 20) {
36
+ const args = clipped(data.arguments)
37
+ const row = { seq: event.seq ?? index, step: identity(data.step) ? data.step : null, name: String(data.name ?? ''), callId: String(data.callId ?? ''),
38
+ atMs: Number.isFinite(event.time) ? event.time : 0, arguments: args.text, result: '', truncated: args.truncated, status: 'pending' }
39
+ tools.push(row); selected.set(row.callId, row)
40
+ }
41
+ }
42
+ if (event.type === 'tool/result') {
43
+ const message = data.message ?? {}, id = message.source?.callId ?? message.toolCallId ?? data.callId
44
+ const row = selected.get(id)
45
+ if (row) {
46
+ const result = clipped(contentText(message.content))
47
+ row.result = result.text; row.truncated ||= result.truncated; row.status = message.isError ? 'error' : 'complete'
48
+ }
49
+ }
50
+ }
51
+ if (event.type === 'turn/end') active = null
52
+ }
53
+ return { found, turn, input, inputTruncated, tools, totalTools, offset }
54
+ }
55
+
56
+ export async function getTurnInspection(ctx, query) {
57
+ if (!query || typeof query.sessionId !== 'string' || !query.sessionId || query.sessionId.length > 512 || !identity(query.turn)
58
+ || !identity(query.offset ?? 0) || (query.offset ?? 0) > 1e7) throw new Error('invalid turn inspection query')
59
+ const live = ctx?.get?.('sessions')?.get?.(query.sessionId)?.snapshotEvents?.()
60
+ if (Array.isArray(live)) return inspectTurn(live, query.turn, query.offset ?? 0)
61
+ const paths = listSessionLogs(join(resolveDshHome(), 'sessions'), new Set([query.sessionId]))
62
+ return inspectTurn(paths.length ? iterateSessionRecords(paths[0]) : [], query.turn, query.offset ?? 0)
63
+ }
@@ -86,7 +86,11 @@ export const sessionBillingSchema = turnCostSchema.extend({ totalCalls: num, off
86
86
  kinds: z.array(statisticsBuckets.extend({ kind: z.string(), unpriced: z.boolean() })),
87
87
  turns: z.array(statisticsBuckets.extend({ turn: num.nullable(), unpriced: z.boolean() })),
88
88
  totalTurns: num, turnOffset: num,
89
+ stepShares: z.object(Object.fromEntries(['cost', 'calls'].map(key => [key, z.array(statisticsBuckets.extend({ turn: num.nullable(), step: num.nullable(), other: z.boolean(), unpriced: z.boolean() }))]))),
89
90
  })
91
+ export const turnInspectionQuerySchema = z.object({ sessionId: z.string().min(1).max(512), turn: z.number().int().min(0), offset: z.number().int().min(0).max(1e7).optional() })
92
+ export const turnInspectionSchema = z.object({ found: z.boolean(), turn: num, input: z.string(), inputTruncated: z.boolean(), totalTools: num, offset: num,
93
+ tools: z.array(z.object({ seq: num, step: num.nullable(), name: z.string(), callId: z.string(), atMs: num, arguments: z.string(), result: z.string(), truncated: z.boolean(), status: z.enum(['pending', 'complete', 'error']) })) })
90
94
 
91
95
  const longContextSchema = z.object({
92
96
  aboveInputTokens: z.number().positive(),
@@ -718,6 +722,8 @@ export const TYPERT = {
718
722
  parameters: [{ name: 'query', wire: 'query', source: 'json', codec: strictCodec('StatisticsQuery', statisticsQuerySchema) }],
719
723
  result: strictCodec(name, schema),
720
724
  })),
725
+ { id: 'dsh-cost-meter#costMeter/getTurnInspection', service: 'costMeter', namespace: 'costMeter', method: 'getTurnInspection', invocation: { kind: 'direct' },
726
+ parameters: [{ name: 'query', wire: 'query', source: 'json', codec: strictCodec('TurnInspectionQuery', turnInspectionQuerySchema) }], result: strictCodec('TurnInspection', turnInspectionSchema) },
721
727
  {
722
728
  // 写入一枚密钥到 DSH 凭据库(v1.6.8):密钥不再经 updateConfig 传递,值只进凭据库。
723
729
  id: 'dsh-cost-meter#costMeter/setCredential',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cost-meter",
3
- "version": "1.8.4",
3
+ "version": "1.8.5",
4
4
  "description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(11 家,含小米 MiMo Token Plan 控制台 Cookie 查询、SCNet Token Plan 本地 Credits 计量与火山方舟 Volcano Ark Coding Plan AK/SK 签名查询)、自定义 Provider 余额查询(可配任意 HTTP 端点)与余额进度条、峰谷计价时段显示与切换前弹窗/系统通知提醒(位置/提前量/类型可配),界面中英双语。Session cost tracking plugin for DeepSeek Harness: per-conversation cost, daily totals, history and official price sync, with multi-vendor model pricing (built-in 90+ model catalog with auto-matching), Coding Plan quota queries & display for 11 vendors (incl. Xiaomi MiMo Token Plan via console cookie, local credits metering for the SCNet Token Plan and Volcano Ark Coding Plan via AK/SK signature), plan/API dual-track billing with per-1% & full-window Token Plan usage estimates, custom provider balance lookup (configurable HTTP endpoint) and balance progress bar, plus peak/off-peak pricing display with pre-switch popup & system-notification alerts (position / lead time / type configurable), in a bilingual (Chinese/English) UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",