d0aa53c79d
全局面板「用量」数据问题修复与澄清: 1. 时区分桶根因(store.ts):按天/周/月分桶与今天/本月窗口起点原全用 UTC, UTC+8 用户本地 00:00–08:00 的 run 被算进前一天、当天窗口晚 8h → 柱状图偏移 一天、当天偏小。新增 localDay/localMonth,periodStartISO/weekStartLabel/ usageDetail 改用 daemon 本地时区(SQL 比绝对时刻仍正确)。 2. UI 口径区分(design/ui_kits/console):额度条标注账号级含交互会话,AGENT 卡/ 详情弹框标注仅统计 maestro 任务执行,$ 加 API 列表价估算非实际扣费提示并以 ≈ 前缀,模型列加近似角标。新增 4 个 i18n key×5 语言。 3. 文档:新增 docs/usage-cost-explained.html(两数据源/为何对不上/cost 公式/ 单价表/本地时区分桶/已知近似),登记进 docs/index.html 知识库调研。 4. 测试:budget.test.ts 新增 UTC+8 跨日分桶用例。typecheck + 268 测试全绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
228 lines
14 KiB
HTML
228 lines
14 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN" data-theme="dark">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>MAESTRO · 用量与成本计算说明</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0a0d0b; --bg-deep: #070908; --panel: #111610; --panel-2: #161c13;
|
||
--line: #1e2a1c; --ink: #c8d4c0; --muted: #7a9070; --faint: #445040;
|
||
--green: #4ade80; --green-dim: rgba(74,222,128,.12);
|
||
--violet: #a78bfa; --violet-dim: rgba(167,139,250,.1);
|
||
--amber: #fbbf24; --amber-dim: rgba(251,191,36,.1);
|
||
--red: #f87171; --red-dim: rgba(248,113,113,.1);
|
||
--cyan: #22d3ee; --cyan-dim: rgba(34,211,238,.1);
|
||
--mono: 'IBM Plex Mono', 'Noto Sans SC', monospace;
|
||
--radius-sm: 4px; --radius-md: 6px;
|
||
}
|
||
[data-theme="light"] {
|
||
--bg: #f4f6f2; --bg-deep: #eaede7; --panel: #ffffff; --panel-2: #f0f2ee;
|
||
--line: #d0d8cc; --ink: #1a2418; --muted: #4a6045; --faint: #8a9e86;
|
||
--green: #16a34a; --green-dim: rgba(22,163,74,.1);
|
||
--violet: #7c3aed; --violet-dim: rgba(124,58,237,.1);
|
||
--amber: #d97706; --amber-dim: rgba(217,119,6,.1);
|
||
--red: #dc2626; --red-dim: rgba(220,38,38,.1);
|
||
--cyan: #0891b2; --cyan-dim: rgba(8,145,178,.1);
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
html, body { height: 100%; }
|
||
body {
|
||
background: var(--bg); color: var(--ink);
|
||
font-family: var(--mono); font-size: 13px; line-height: 1.7; padding: 32px;
|
||
}
|
||
body::before {
|
||
content: ''; position: fixed; inset: 0; z-index: 0; pointer-events: none;
|
||
background:
|
||
repeating-linear-gradient(0deg, rgba(0,0,0,.14) 0 1px, transparent 1px 3px),
|
||
radial-gradient(ellipse at 50% 40%, transparent 55%, rgba(0,0,0,.45));
|
||
opacity: .45;
|
||
}
|
||
[data-theme="light"] body::before { opacity: .07; }
|
||
.page { position: relative; z-index: 1; max-width: 1000px; margin: 0 auto; }
|
||
|
||
.header {
|
||
display: flex; align-items: baseline; gap: 16px;
|
||
border-bottom: 1px solid var(--line); padding-bottom: 20px; margin-bottom: 28px;
|
||
}
|
||
.logo { font-size: 18px; font-weight: 700; letter-spacing: .25em; color: var(--green); text-shadow: 0 0 16px var(--green); }
|
||
.logo span { animation: blink 1.1s steps(1) infinite; }
|
||
@keyframes blink { 50% { opacity: 0; } }
|
||
.subtitle { color: var(--muted); font-size: 11px; letter-spacing: .2em; }
|
||
.theme-btn {
|
||
margin-left: auto; background: var(--panel); border: 1px solid var(--line);
|
||
color: var(--muted); font-family: var(--mono); font-size: 11px;
|
||
padding: 4px 10px; border-radius: var(--radius-sm); cursor: pointer; letter-spacing: .1em;
|
||
}
|
||
.theme-btn:hover { color: var(--ink); border-color: var(--muted); }
|
||
|
||
.lead { color: var(--muted); font-size: 13px; line-height: 1.9; margin-bottom: 8px; }
|
||
.lead strong { color: var(--ink); }
|
||
|
||
.sec-head {
|
||
display: flex; align-items: center; gap: 8px;
|
||
font-size: 10px; font-weight: 600; letter-spacing: .28em;
|
||
color: var(--muted); text-transform: uppercase;
|
||
margin-bottom: 16px; margin-top: 40px;
|
||
}
|
||
.sec-mark { width: 3px; height: 14px; border-radius: 2px; }
|
||
|
||
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
@media (max-width: 720px) { .grid2 { grid-template-columns: 1fr; } }
|
||
.card {
|
||
border: 1px solid var(--line); border-radius: var(--radius-md);
|
||
background: var(--panel); padding: 16px 18px;
|
||
}
|
||
.card.src-a { border-color: rgba(34,211,238,.4); background: linear-gradient(135deg, var(--panel), rgba(34,211,238,.04)); }
|
||
.card.src-b { border-color: rgba(74,222,128,.4); background: linear-gradient(135deg, var(--panel), rgba(74,222,128,.04)); }
|
||
.card h3 { font-size: 12px; font-weight: 700; letter-spacing: .1em; margin-bottom: 4px; }
|
||
.card.src-a h3 { color: var(--cyan); }
|
||
.card.src-b h3 { color: var(--green); }
|
||
.card .tag { font-size: 10px; color: var(--faint); letter-spacing: .12em; display: block; margin-bottom: 10px; }
|
||
.card ul { list-style: none; display: flex; flex-direction: column; gap: 7px; }
|
||
.card li { font-size: 12px; color: var(--muted); display: flex; gap: 8px; align-items: flex-start; line-height: 1.6; }
|
||
.card li::before { content: '→'; color: var(--faint); flex-shrink: 0; }
|
||
.card li b { color: var(--ink); font-weight: 600; }
|
||
code { background: var(--bg-deep); border: 1px solid var(--line); border-radius: 3px; padding: 1px 5px; font-size: 12px; color: var(--amber); }
|
||
|
||
.callout {
|
||
border-left: 3px solid var(--amber); background: var(--amber-dim);
|
||
border-radius: var(--radius-sm); padding: 12px 16px; margin: 16px 0;
|
||
font-size: 12px; color: var(--ink); line-height: 1.8;
|
||
}
|
||
.callout.violet { border-left-color: var(--violet); background: var(--violet-dim); }
|
||
.callout b { color: var(--ink); }
|
||
|
||
.formula {
|
||
background: var(--bg-deep); border: 1px solid var(--line); border-radius: var(--radius-md);
|
||
padding: 16px 18px; font-size: 13px; color: var(--ink); margin: 12px 0; line-height: 1.9;
|
||
}
|
||
.formula .op { color: var(--cyan); }
|
||
.formula small { display: block; color: var(--faint); font-size: 11px; margin-top: 8px; }
|
||
|
||
table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 12px; }
|
||
th, td { padding: 7px 10px; text-align: left; border-bottom: 1px solid var(--line); }
|
||
th { color: var(--muted); font-weight: 600; font-size: 10px; letter-spacing: .12em; text-transform: uppercase; }
|
||
td { color: var(--ink); }
|
||
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||
tbody tr:hover { background: var(--panel-2); }
|
||
.model-name { color: var(--cyan); font-weight: 600; }
|
||
|
||
ul.plain { list-style: none; display: flex; flex-direction: column; gap: 8px; margin: 8px 0; }
|
||
ul.plain li { font-size: 12px; color: var(--muted); display: flex; gap: 8px; align-items: flex-start; line-height: 1.7; }
|
||
ul.plain li::before { content: '▍'; color: var(--green); flex-shrink: 0; font-size: 10px; }
|
||
ul.plain li b { color: var(--ink); }
|
||
|
||
.foot { margin-top: 44px; font-size: 11px; color: var(--faint); border-top: 1px solid var(--line); padding-top: 14px; line-height: 1.8; }
|
||
.src-ref { color: var(--faint); font-size: 11px; }
|
||
a { color: var(--cyan); }
|
||
</style>
|
||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
|
||
<div class="header">
|
||
<div class="logo">MAESTRO<span>▮</span></div>
|
||
<div class="subtitle">用量与成本计算说明 · USAGE & COST EXPLAINED</div>
|
||
<button class="theme-btn" onclick="document.documentElement.dataset.theme=document.documentElement.dataset.theme==='dark'?'light':'dark'">⊙ 切换主题</button>
|
||
</div>
|
||
|
||
<p class="lead">控制台里的「用量」其实来自<strong>两个互不相交的数据源</strong>,口径不同、覆盖范围不同,所以经常看着「对不上」。本文把两者来源、为什么对不上、成本怎么算、按天/周/月怎么分桶讲清楚。</p>
|
||
|
||
<!-- ── 两个数据源 ── -->
|
||
<div class="sec-head"><div class="sec-mark" style="background:var(--cyan)"></div>两个数据源 · TWO SOURCES</div>
|
||
|
||
<div class="grid2">
|
||
<div class="card src-a">
|
||
<h3>A · 账号级订阅额度</h3>
|
||
<span class="tag">额度条 / QuotaMeter(顶栏)</span>
|
||
<ul>
|
||
<li>daemon 直连 <code>GET api.anthropic.com/api/oauth/usage</code>,取 <b>five_hour / seven_day</b> 的利用率百分比。</li>
|
||
<li>覆盖范围 = <b>整个 Claude Code 账号</b>:你手敲的交互会话、别的项目、别的工具,全部计入。</li>
|
||
<li>maestro 只能拿到这个「总量百分比」,<b>拿不到明细</b>,也无法拆出「哪部分是 maestro 烧的」。</li>
|
||
<li>来源:<span class="src-ref">src/daemon/usage.ts</span></li>
|
||
</ul>
|
||
</div>
|
||
<div class="card src-b">
|
||
<h3>B · maestro 自有 run 的 cost / token</h3>
|
||
<span class="tag">AGENT 卡 2×2 + 用量详情弹框</span>
|
||
<ul>
|
||
<li>每次 maestro worker 跑 CC 调用,从 result 读 <b>四类 token</b> 写进 <code>outbox.ndjson</code>,daemon 折算累加进 <code>runs.cost_usd / runs.usage</code>。</li>
|
||
<li>覆盖范围 = <b>仅 maestro 的任务执行</b>(executor + 双复审 + 解冲突等),你自己的会话<b>不在内</b>。</li>
|
||
<li>天然比额度条少 —— 它只统计自己发起的 run。</li>
|
||
<li>来源:<span class="src-ref">cc.ts → ingest.ts → store.setRunUsage() / costSummary() / usageDetail()</span></li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="callout violet">
|
||
<b>「最近只有 maestro 在跑,却还有其他 token 消费」的真相:</b><br>
|
||
那是你<b>自己用 Claude Code 烧的额度</b>(交互会话 / 其他工具)。它只反映在<b>数据源 A(账号级额度条)</b>里,maestro 采集不到、也算不进数据源 B 的明细,但额度条照样会动。<b>这不是 bug,是两个数据源口径不同。</b> 控制台已分别标注:额度条标「账号级,含交互会话」,AGENT 卡 / 详情标「仅统计 maestro 任务执行」。
|
||
</div>
|
||
|
||
<!-- ── 成本如何计算 ── -->
|
||
<div class="sec-head"><div class="sec-mark" style="background:var(--green)"></div>成本如何计算 · COST FORMULA</div>
|
||
|
||
<p class="lead">数据源 B 的成本是<strong>逐次 CC 调用精确折算后累加</strong>的。公式:</p>
|
||
|
||
<div class="formula">
|
||
成本(USD) = <span class="op">Σ</span>( input·P<sub>in</sub> <span class="op">+</span> output·P<sub>out</sub> <span class="op">+</span> cacheRead·P<sub>cr</sub> <span class="op">+</span> cacheWrite·P<sub>cw</sub> ) <span class="op">/</span> 1,000,000
|
||
<small>四类 token 各按其模型单价折算;单价单位 = USD / 每 1M token。一个 run 可能含多次 CC 调用(executor + 双复审 / resume / 回退),每次按各自模型精确折算后求和 → <code>runs.cost_usd</code> 准确。来源:src/model/pricing.ts · store.setRunUsage()</small>
|
||
</div>
|
||
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>模型</th>
|
||
<th class="num">input</th>
|
||
<th class="num">output</th>
|
||
<th class="num">cacheRead</th>
|
||
<th class="num">cacheWrite</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr><td class="model-name">claude-opus-4-8</td><td class="num">15</td><td class="num">75</td><td class="num">1.5</td><td class="num">18.75</td></tr>
|
||
<tr><td class="model-name">claude-sonnet-4-6</td><td class="num">3</td><td class="num">15</td><td class="num">0.3</td><td class="num">3.75</td></tr>
|
||
<tr><td class="model-name">claude-haiku-4-5</td><td class="num">1</td><td class="num">5</td><td class="num">0.1</td><td class="num">1.25</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="lead" style="font-size:11px">单价单位:USD / 1M token。模型名归一:精确命中优先,否则按已知前缀匹配(容忍日期 / 区域后缀,如 <code>claude-opus-4-8-20260101</code>)。源:<span class="src-ref">src/model/pricing.ts · MODEL_PRICES</span></p>
|
||
|
||
<div class="callout">
|
||
<b>$ 是按 API 列表价的名义估算,不是订阅实际扣费。</b> 它衡量「这些 run 若按 API 计价值多少钱」,便于横向比较项目/模型开销;你的实际账单取决于订阅套餐,二者不应直接相等。控制台 <code>$</code> 处标注「按 API 列表价估算,非订阅实际扣费」,并以 <code>≈</code> 前缀提示。
|
||
</div>
|
||
|
||
<!-- ── 分桶与时区 ── -->
|
||
<div class="sec-head"><div class="sec-mark" style="background:var(--amber)"></div>按天 / 周 / 月分桶与时区 · BUCKETING</div>
|
||
|
||
<ul class="plain">
|
||
<li><b>按天</b>:近 30 天,桶 = 本地 <code>YYYY-MM-DD</code>。</li>
|
||
<li><b>按周</b>:近 12 周,桶 = 本地<b>周一</b> <code>YYYY-MM-DD</code>(滚动 7 天窗)。</li>
|
||
<li><b>按月</b>:近 12 个月,桶 = 本地 <code>YYYY-MM</code>。</li>
|
||
<li>「今天 / 本月」窗口起点(<code>periodStartISO</code>)、预算护栏窗口同样以<b>本地零点</b>为基准。</li>
|
||
</ul>
|
||
|
||
<div class="callout">
|
||
<b>本次修复(时区分桶):</b> 此前分桶与窗口起点全用 <b>UTC</b>,UTC+8 用户在本地 00:00–08:00 跑的 run 会被算进<b>前一天</b>、当天窗口晚 8 小时开始 → 柱状图整体偏移一天、当天偏小。现已统一改为 <b>daemon 本地时区</b>(local-first 单机,daemon 时区即用户时区,零配置)。SQL 比较的是绝对时刻,本地零点转 ISO 后比较仍正确。源:<span class="src-ref">store.ts · localDay / localMonth / periodStartISO / weekStartLabel / usageDetail</span>
|
||
</div>
|
||
|
||
<!-- ── 已知近似 ── -->
|
||
<div class="sec-head"><div class="sec-mark" style="background:var(--red)"></div>已知近似 · KNOWN APPROXIMATIONS</div>
|
||
|
||
<ul class="plain">
|
||
<li><b>分模型 token 为近似。</b> <code>setRunUsage</code> 把累计 token 连同<b>最后一次</b> CC 调用的 model 一起存(<code>{...usage, model}</code>);总 <code>cost_usd</code> 逐次精确折算 → <b>总花费准</b>,但 byModel 的 token / cost 拆分按「run 末次模型」归集,<b>不精确</b>。控制台模型列以 <code>≈</code> 角标提示。</li>
|
||
<li><b>未知模型记 0。</b> 若配置了非 <code>opus-4-8 / sonnet-4-6 / haiku-4-5</code> 前缀的模型,<code>priceOf</code> 命中失败 → <code>computeCost</code> 返回 0(不阻断执行,仅成本记 0)。</li>
|
||
<li><b>$ 为名义估算</b>,非订阅实账单(见上)。</li>
|
||
</ul>
|
||
|
||
<div class="foot">
|
||
单一事实源:成本计价集中于 <code>src/model/pricing.ts</code>,分桶 / 窗口集中于 <code>src/store/store.ts</code>。<br>
|
||
数据源 A(额度):<code>src/daemon/usage.ts</code>;数据源 B(明细):<code>cc.ts → ingest.ts → store.ts</code>。<br>
|
||
本文随实现变化更新;新增 / 迁移文档请同步登记 <code>docs/index.html</code>。
|
||
</div>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|