merge: maestro/tsk_JKAzHuXvSw7h [tsk_JKAzHuXvSw7h]
This commit is contained in:
@@ -258,16 +258,18 @@ function AgentCard({ global, summary, t, collapsed, projects }) {
|
||||
<span style={{ color: 'var(--cyan)', display: 'inline-flex' }}><IcoAgents /></span>{t.gAgents.toUpperCase()}
|
||||
<span style={{ marginLeft: 'auto', fontWeight: 400, letterSpacing: '.04em', color: 'var(--faint)' }}>{t.apWeek}</span>
|
||||
</div>
|
||||
{/* 口径说明:本卡仅统计 maestro 任务执行,与账号级额度条不同源 */}
|
||||
<div style={{ fontSize: 9.5, color: 'var(--faint)', letterSpacing: '.04em', marginBottom: 8, marginTop: -4 }}>{t.apScopeMaestro}</div>
|
||||
{/* 概览 2×2 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1, background: 'var(--line-soft)', border: '1px solid var(--line-soft)', borderRadius: 4, overflow: 'hidden', marginBottom: 10 }}>
|
||||
{[
|
||||
[fmtTokens(summary.tokensWeek), t.apTokens, 'var(--cyan)'],
|
||||
['$' + summary.costWeek.toFixed(1), t.apCost, 'var(--green)'],
|
||||
[summary.runsWeek, t.apRuns, 'var(--ink)'],
|
||||
[summary.activeNow + ' / ' + global.maxAgents, t.apActive, summary.activeNow ? 'var(--cyan)' : 'var(--faint)'],
|
||||
].map(([v, k, c], i) => (
|
||||
[fmtTokens(summary.tokensWeek), t.apTokens, 'var(--cyan)', null],
|
||||
['≈$' + summary.costWeek.toFixed(1), t.apCost, 'var(--green)', t.apCostEstimateHint],
|
||||
[summary.runsWeek, t.apRuns, 'var(--ink)', null],
|
||||
[summary.activeNow + ' / ' + global.maxAgents, t.apActive, summary.activeNow ? 'var(--cyan)' : 'var(--faint)', null],
|
||||
].map(([v, k, c, hint], i) => (
|
||||
<div key={i} style={{ background: 'var(--bg-deep)', padding: '7px 9px' }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, color: c, letterSpacing: '.02em' }}>{v}</div>
|
||||
<div title={hint || undefined} style={{ fontSize: 15, fontWeight: 700, color: c, letterSpacing: '.02em' }}>{v}</div>
|
||||
<div style={{ fontSize: 9.5, color: 'var(--muted)', letterSpacing: '.06em', marginTop: 1 }}>{k}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -319,6 +321,7 @@ function AgentDetailModal({ projects, t, onClose }) {
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
|
||||
<span style={{ color: 'var(--cyan)', display: 'inline-flex' }}><IcoAgents /></span>
|
||||
<span style={{ fontSize: 14, fontWeight: 700, letterSpacing: '.04em', color: 'var(--ink)' }}>{t.apDetailTitle}</span>
|
||||
<span style={{ fontSize: 9.5, color: 'var(--faint)', letterSpacing: '.04em' }}>· {t.apScopeMaestro}</span>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 1, background: 'var(--line-soft)', border: '1px solid var(--line-soft)', borderRadius: 4, overflow: 'hidden' }}>
|
||||
{[['day', t.apByDay], ['week', t.apByWeek], ['month', t.apByMonth]].map(([g, label]) => (
|
||||
<button key={g} type="button" onClick={() => setGran(g)} style={{
|
||||
@@ -334,13 +337,13 @@ function AgentDetailModal({ projects, t, onClose }) {
|
||||
{/* total 概览 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 1, background: 'var(--line-soft)', border: '1px solid var(--line-soft)', borderRadius: 4, overflow: 'hidden', marginBottom: 14 }}>
|
||||
{[
|
||||
[fmtUsd(tot.costUsd), t.apCost, 'var(--green)'],
|
||||
[fmtTokens(tot.tokens), t.apTokens, 'var(--cyan)'],
|
||||
[tot.runs, t.apRuns, 'var(--ink)'],
|
||||
[tot.tasks, t.apColTasks, 'var(--ink)'],
|
||||
].map(([v, k, c], i) => (
|
||||
[fmtUsd(tot.costUsd), t.apCost, 'var(--green)', t.apCostEstimateHint],
|
||||
[fmtTokens(tot.tokens), t.apTokens, 'var(--cyan)', null],
|
||||
[tot.runs, t.apRuns, 'var(--ink)', null],
|
||||
[tot.tasks, t.apColTasks, 'var(--ink)', null],
|
||||
].map(([v, k, c, hint], i) => (
|
||||
<div key={i} style={{ background: 'var(--bg-deep)', padding: '8px 10px' }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 700, color: c }}>{v}</div>
|
||||
<div title={hint || undefined} style={{ fontSize: 15, fontWeight: 700, color: c }}>{v}</div>
|
||||
<div style={{ fontSize: 9.5, color: 'var(--muted)', letterSpacing: '.06em', marginTop: 1 }}>{k}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -372,7 +375,7 @@ function AgentDetailModal({ projects, t, onClose }) {
|
||||
<th style={{ padding: '4px 6px', fontWeight: 600, textAlign: 'right' }}>{t.apTokens}</th>
|
||||
<th style={{ padding: '4px 6px', fontWeight: 600, textAlign: 'right' }}>{t.apCost}</th>
|
||||
<th style={{ padding: '4px 6px', fontWeight: 600, textAlign: 'right' }}>{t.apColTasks}</th>
|
||||
<th style={{ padding: '4px 6px', fontWeight: 600, textAlign: 'left' }}>{t.apColModels}</th>
|
||||
<th title={t.apModelApprox} style={{ padding: '4px 6px', fontWeight: 600, textAlign: 'left', cursor: 'help' }}>{t.apColModels} <span style={{ color: 'var(--faint)' }}>≈</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -164,9 +164,10 @@ function AgentSection({ agents, quota, t, onOpenStream }) {
|
||||
{!folded ? (<React.Fragment>
|
||||
{quota ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 22, flexWrap: 'wrap', fontFamily: 'var(--mono)', margin: '0 0 12px' }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, color: 'var(--muted)', letterSpacing: '.08em' }}>{t.quota}</span>
|
||||
<span title={t.quotaScopeAccount} style={{ fontSize: 11, fontWeight: 600, color: 'var(--muted)', letterSpacing: '.08em', cursor: 'help' }}>{t.quota}</span>
|
||||
<QuotaMeter label="5h" pct={quota.five.pct} detail={t.quotaReset.replace('{t}', quota.five.reset)} />
|
||||
<QuotaMeter label={t.quotaWeek} pct={quota.week.pct} detail={t.quotaReset.replace('{t}', quota.week.reset)} />
|
||||
<span style={{ fontSize: 9.5, color: 'var(--faint)', letterSpacing: '.04em' }}>{t.quotaScopeAccount}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{agents.length === 0 ? (
|
||||
|
||||
@@ -27,6 +27,7 @@ window.MAESTRO_I18N = {
|
||||
gAgents: '全局 Agent', gAgentsDetail: '{p}/{P} 个项目运行 · {a} 个 agent',
|
||||
apActive: '运行', apRuns: '次运行', apTokens: 'token', apWeek: '本周', apCost: '花费', apTotal: '所有项目', apPerProj: '按项目', apIdle: '空闲', apMore: '展开其余 {n} 个', apCollapse: '收起',
|
||||
apDetail: '详情', apDetailTitle: '用量详情', apByDay: '按天', apByWeek: '按周', apByMonth: '按月', apColProject: '项目', apColTasks: '任务数', apColModels: '模型', apEmpty: '暂无用量数据',
|
||||
apScopeMaestro: '仅统计 maestro 任务执行', quotaScopeAccount: '整个账号(含你自己的 Claude Code 会话)', apCostEstimateHint: '按 API 列表价估算,非订阅实际扣费', apModelApprox: '分模型 token 为近似(按 run 末次模型归集)',
|
||||
gConfig: '全局配置', gConfigDetail: '并发上限 {n} · daemon {v}',
|
||||
gUserPlan: '订阅', gUserSignOut: '退出登录', gUserSettings: '账户设置',
|
||||
gGlobalSettings: '全局设置', gSettingsHint: '以下为新建项目的默认值(按用户保存)', gMaxRetries: '最大重试', gTimeoutMin: '超时(分钟)', gAutoPlan: '拆解自动放行', gAutoExec: '执行自动放行', gBudget: '预算 $(空=不限)', gBudgetPeriod: '预算周期', gPeriodDay: '按天', gPeriodMonth: '按月',
|
||||
@@ -81,6 +82,7 @@ window.MAESTRO_I18N = {
|
||||
gAgents: 'Agents', gAgentsDetail: '{p}/{P} projects · {a} agents',
|
||||
apActive: 'active', apRuns: 'runs', apTokens: 'tokens', apWeek: 'this week', apCost: 'cost', apTotal: 'All projects', apPerProj: 'BY PROJECT', apIdle: 'idle', apMore: '+{n} more', apCollapse: 'Collapse',
|
||||
apDetail: 'Details', apDetailTitle: 'Usage detail', apByDay: 'By day', apByWeek: 'By week', apByMonth: 'By month', apColProject: 'Project', apColTasks: 'Tasks', apColModels: 'Models', apEmpty: 'No usage data yet',
|
||||
apScopeMaestro: 'Maestro tasks only', quotaScopeAccount: 'Whole account (incl. your own Claude Code sessions)', apCostEstimateHint: 'Estimated at API list price, not actual subscription billing', apModelApprox: 'Per-model tokens are approximate (attributed to each run’s last model)',
|
||||
gConfig: 'Settings', gConfigDetail: 'Max concurrency {n} · daemon {v}',
|
||||
gUserPlan: 'Plan', gUserSignOut: 'Sign out', gUserSettings: 'Account settings',
|
||||
gGlobalSettings: 'Global settings', gSettingsHint: 'Defaults applied to new projects (saved per user)', gMaxRetries: 'Max retries', gTimeoutMin: 'Timeout (min)', gAutoPlan: 'Auto-approve plan', gAutoExec: 'Auto-approve exec', gBudget: 'Budget $ (empty=∞)', gBudgetPeriod: 'Budget period', gPeriodDay: 'Daily', gPeriodMonth: 'Monthly',
|
||||
@@ -135,6 +137,7 @@ window.MAESTRO_I18N = {
|
||||
gAgents: 'Agents globales', gAgentsDetail: '{p}/{P} proyectos · {a} agents',
|
||||
apActive: 'activos', apRuns: 'ejecuciones', apTokens: 'tokens', apWeek: 'esta semana', apCost: 'coste', apTotal: 'Todos los proyectos', apPerProj: 'POR PROYECTO', apIdle: 'inactivo', apMore: '+{n} más', apCollapse: 'Contraer',
|
||||
apDetail: 'Detalles', apDetailTitle: 'Detalle de uso', apByDay: 'Por día', apByWeek: 'Por semana', apByMonth: 'Por mes', apColProject: 'Proyecto', apColTasks: 'Tareas', apColModels: 'Modelos', apEmpty: 'Sin datos de uso',
|
||||
apScopeMaestro: 'Solo tareas de Maestro', quotaScopeAccount: 'Toda la cuenta (incl. tus propias sesiones de Claude Code)', apCostEstimateHint: 'Estimado al precio de lista de la API, no la facturación real de la suscripción', apModelApprox: 'Los tokens por modelo son aproximados (atribuidos al último modelo de cada run)',
|
||||
gConfig: 'Config global', gConfigDetail: 'Concurrencia máx {n} · daemon {v}',
|
||||
gUserPlan: 'Plan', gUserSignOut: 'Cerrar sesión', gUserSettings: 'Ajustes de cuenta',
|
||||
gGlobalSettings: 'Configuración global', gSettingsHint: 'Valores por defecto para proyectos nuevos (por usuario)', gMaxRetries: 'Reintentos máx.', gTimeoutMin: 'Tiempo límite (min)', gAutoPlan: 'Auto-aprobar plan', gAutoExec: 'Auto-aprobar ejec.', gBudget: 'Presupuesto $ (vacío=∞)', gBudgetPeriod: 'Periodo', gPeriodDay: 'Diario', gPeriodMonth: 'Mensual',
|
||||
@@ -189,6 +192,7 @@ window.MAESTRO_I18N = {
|
||||
gAgents: 'グローバル Agent', gAgentsDetail: '{p}/{P} プロジェクト · {a} 個の agent',
|
||||
apActive: '実行中', apRuns: '回実行', apTokens: 'token', apWeek: '今週', apCost: 'コスト', apTotal: '全プロジェクト', apPerProj: 'プロジェクト別', apIdle: 'アイドル', apMore: '他 {n} 件', apCollapse: '折りたたむ',
|
||||
apDetail: '詳細', apDetailTitle: '使用量の詳細', apByDay: '日別', apByWeek: '週別', apByMonth: '月別', apColProject: 'プロジェクト', apColTasks: 'タスク数', apColModels: 'モデル', apEmpty: '使用量データなし',
|
||||
apScopeMaestro: 'maestro タスクのみ', quotaScopeAccount: 'アカウント全体(自分の Claude Code セッションを含む)', apCostEstimateHint: 'API 定価による概算で、実際のサブスク請求額ではありません', apModelApprox: 'モデル別 token は概算(各 run の最終モデルに集計)',
|
||||
gConfig: 'グローバル設定', gConfigDetail: '並列上限 {n} · daemon {v}',
|
||||
gUserPlan: 'プラン', gUserSignOut: 'ログアウト', gUserSettings: 'アカウント設定',
|
||||
gGlobalSettings: 'グローバル設定', gSettingsHint: '新規プロジェクトの既定値(ユーザー単位で保存)', gMaxRetries: '最大リトライ', gTimeoutMin: 'タイムアウト(分)', gAutoPlan: '分解を自動承認', gAutoExec: '実行を自動承認', gBudget: '予算 $(空=無制限)', gBudgetPeriod: '予算期間', gPeriodDay: '日次', gPeriodMonth: '月次',
|
||||
@@ -243,6 +247,7 @@ window.MAESTRO_I18N = {
|
||||
gAgents: 'Agents globaux', gAgentsDetail: '{p}/{P} projets · {a} agents',
|
||||
apActive: 'actifs', apRuns: 'exécutions', apTokens: 'tokens', apWeek: 'cette semaine', apCost: 'coût', apTotal: 'Tous les projets', apPerProj: 'PAR PROJET', apIdle: 'inactif', apMore: '+{n} de plus', apCollapse: 'Réduire',
|
||||
apDetail: 'Détails', apDetailTitle: "Détail d'usage", apByDay: 'Par jour', apByWeek: 'Par semaine', apByMonth: 'Par mois', apColProject: 'Projet', apColTasks: 'Tâches', apColModels: 'Modèles', apEmpty: "Aucune donnée d'usage",
|
||||
apScopeMaestro: 'Tâches Maestro uniquement', quotaScopeAccount: 'Tout le compte (y compris vos propres sessions Claude Code)', apCostEstimateHint: 'Estimé au prix catalogue de l’API, pas la facturation réelle de l’abonnement', apModelApprox: 'Les tokens par modèle sont approximatifs (attribués au dernier modèle de chaque run)',
|
||||
gConfig: 'Config globale', gConfigDetail: 'Concurrence max {n} · daemon {v}',
|
||||
gUserPlan: 'Forfait', gUserSignOut: 'Se déconnecter', gUserSettings: 'Paramètres du compte',
|
||||
gGlobalSettings: 'Paramètres globaux', gSettingsHint: 'Valeurs par défaut des nouveaux projets (par utilisateur)', gMaxRetries: 'Essais max', gTimeoutMin: 'Délai (min)', gAutoPlan: 'Auto-approuver plan', gAutoExec: 'Auto-approuver exéc.', gBudget: 'Budget $ (vide=∞)', gBudgetPeriod: 'Période', gPeriodDay: 'Quotidien', gPeriodMonth: 'Mensuel',
|
||||
|
||||
+7
-1
@@ -86,7 +86,13 @@ body{font-family:'IBM Plex Mono',monospace;background:var(--bg);color:var(--text
|
||||
|
||||
<!-- ── 知识库调研 ── -->
|
||||
<div class="cat">
|
||||
<div class="cat-hdr"><span class="ico">调研</span> 知识库调研 <span class="cnt">· 1</span></div>
|
||||
<div class="cat-hdr"><span class="ico">调研</span> 知识库调研 <span class="cnt">· 2</span></div>
|
||||
<a class="doc" href="usage-cost-explained.html">
|
||||
<div class="doc-ttl">用量与成本计算说明 · 数据源口径 + cost 公式 + 时区分桶</div>
|
||||
<div class="doc-path">docs/usage-cost-explained.html</div>
|
||||
<div class="doc-desc">讲清全局面板「用量」的<strong>两个互不相交数据源</strong>:① 账号级订阅额度(<code>usage.ts</code> 直连 oauth/usage,含你自己的 Claude Code 交互会话)vs ② maestro 自有 run 的 cost/token(<code>cc.ts→ingest→store</code>,仅任务执行)——这就是「只有 maestro 在跑却有其他 token 消费」的真相。含 <strong>cost 公式</strong>(四类 token × 各模型单价 / 1e6,引自 <code>pricing.ts</code> 单价表)、<strong>按天/周/月分桶规则与本地时区口径</strong>(修复 UTC 偏移一天 bug)、<strong>$ 为 API 列表价名义估算非订阅实账单</strong>、分模型 token 近似与未知模型记 0 两项已知近似。</div>
|
||||
<div class="tags"><span class="tag html">HTML</span><span class="tag design">用量/成本</span></div>
|
||||
</a>
|
||||
<a class="doc" href="task-rules.html">
|
||||
<div class="doc-ttl">任务执行规则参考 · 当前实现</div>
|
||||
<div class="doc-path">docs/task-rules.html</div>
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
<!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>
|
||||
+25
-12
@@ -42,21 +42,34 @@ const SETTINGS_DEFAULT: UserSettings = {
|
||||
};
|
||||
const id = (prefix: string): string => `${prefix}_${nanoid(12)}`;
|
||||
|
||||
/** 预算周期起点(UTC):day=今日零点 / month=当月 1 号零点。与存储的 ISO 时间戳同基准比较。 */
|
||||
const pad2 = (n: number): string => String(n).padStart(2, '0');
|
||||
/** 按 daemon 本地时区把日期格式化为 YYYY-MM-DD(用作「按天」时间桶标签)。 */
|
||||
function localDay(d: Date): string {
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
||||
}
|
||||
/** 按 daemon 本地时区把日期格式化为 YYYY-MM(用作「按月」时间桶标签)。 */
|
||||
function localMonth(d: Date): string {
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预算周期起点(daemon 本地时区):day=今日本地零点 / month=当月 1 号本地零点。
|
||||
* 本地零点经 toISOString() 转回绝对时刻,与存储的 ISO 时间戳同基准比较仍正确。
|
||||
*/
|
||||
function periodStartISO(period: 'day' | 'week' | 'month'): string {
|
||||
const d = new Date();
|
||||
if (period === 'week') return new Date(Date.now() - 7 * 86400_000).toISOString();
|
||||
const start = period === 'day'
|
||||
? Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())
|
||||
: Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
|
||||
return new Date(start).toISOString();
|
||||
? new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
||||
: new Date(d.getFullYear(), d.getMonth(), 1);
|
||||
return start.toISOString();
|
||||
}
|
||||
|
||||
/** 把 ISO 时间戳归一到所属周的周一(UTC),返回 YYYY-MM-DD,作为「按周」时间桶标签。 */
|
||||
/** 把 ISO 时间戳归一到所属周的周一(daemon 本地时区),返回本地 YYYY-MM-DD,作为「按周」时间桶标签。 */
|
||||
function weekStartLabel(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
const dow = (d.getUTCDay() + 6) % 7; // 周一=0 … 周日=6
|
||||
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - dow)).toISOString().slice(0, 10);
|
||||
const dow = (d.getDay() + 6) % 7; // 周一=0 … 周日=6
|
||||
return localDay(new Date(d.getFullYear(), d.getMonth(), d.getDate() - dow));
|
||||
}
|
||||
|
||||
export class StoreError extends Error {}
|
||||
@@ -1265,8 +1278,8 @@ export class Store {
|
||||
}
|
||||
|
||||
/**
|
||||
* 用量详情:时间序列(按天/按周/按月分桶)+ 分项目下钻(token/花费/任务数/模型)。
|
||||
* day → 近 30 天、桶=YYYY-MM-DD;week → 近 12 周、桶=周一 YYYY-MM-DD;month → 近 12 个月、桶=YYYY-MM。
|
||||
* 用量详情:时间序列(按天/按周/按月分桶,均按 daemon 本地时区)+ 分项目下钻(token/花费/任务数/模型)。
|
||||
* day → 近 30 天、桶=本地 YYYY-MM-DD;week → 近 12 周、桶=本地周一 YYYY-MM-DD;month → 近 12 个月、桶=本地 YYYY-MM。
|
||||
* token/model 解析与 costSummary 同款(usage JSON 存累计 token + 最近 model,model 维度为近似)。
|
||||
*/
|
||||
usageDetail(granularity: 'day' | 'week' | 'month'): UsageDetail {
|
||||
@@ -1275,7 +1288,7 @@ export class Store {
|
||||
? new Date(Date.now() - 30 * 86400_000).toISOString()
|
||||
: granularity === 'week'
|
||||
? new Date(Date.now() - 12 * 7 * 86400_000).toISOString()
|
||||
: new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 11, 1)).toISOString();
|
||||
: new Date(now.getFullYear(), now.getMonth() - 11, 1).toISOString();
|
||||
const rows = this.db.prepare(
|
||||
`SELECT t.project_id AS pid, r.task_id AS tid, r.started_at AS at, r.usage AS usage, r.cost_usd AS cost
|
||||
FROM runs r JOIN tasks t ON t.id = r.task_id
|
||||
@@ -1290,9 +1303,9 @@ export class Store {
|
||||
const u = row.usage ? JSON.parse(row.usage) as UsageTokens & { model?: string } : null;
|
||||
const tokens = u ? u.input + u.output + u.cacheRead + u.cacheWrite : 0;
|
||||
const model = u?.model ?? 'unknown';
|
||||
const bucket = granularity === 'day' ? row.at.slice(0, 10)
|
||||
const bucket = granularity === 'day' ? localDay(new Date(row.at))
|
||||
: granularity === 'week' ? weekStartLabel(row.at)
|
||||
: row.at.slice(0, 7);
|
||||
: localMonth(new Date(row.at));
|
||||
|
||||
const s = series.get(bucket) ?? { cost: 0, tokens: 0, runs: 0 };
|
||||
s.cost += row.cost; s.tokens += tokens; s.runs += 1; series.set(bucket, s);
|
||||
|
||||
@@ -42,6 +42,46 @@ test('store: setRunUsage 累加成本 + costSummary 聚合', () => {
|
||||
s.close();
|
||||
});
|
||||
|
||||
test('store: usageDetail/costSummary 按 daemon 本地时区分桶(UTC+8 跨日不再偏移一天)', () => {
|
||||
const origTZ = process.env.TZ;
|
||||
process.env.TZ = 'Asia/Shanghai'; // 固定 UTC+8,制造 UTC 日 != 本地日
|
||||
try {
|
||||
const s = new Store(':memory:');
|
||||
const p = s.createProject({ name: 'tz', repoPath: '/tmp/tz-' + Math.random() });
|
||||
const t = s.createTask({ projectId: p.id, title: 'x', complexity: 'easy' });
|
||||
|
||||
// —— 按天分桶:近 2 天内 UTC 18:00 的 run → 本地为次日 02:00,本地日应比 UTC 日晚一天 ——
|
||||
const r1 = s.startRun(t.id, 'executor');
|
||||
s.setRunUsage(r1.id, { input: 1_000_000, output: 0, cacheRead: 0, cacheWrite: 0 }, 'claude-opus-4-8');
|
||||
const base = new Date();
|
||||
base.setUTCDate(base.getUTCDate() - 2);
|
||||
base.setUTCHours(18, 0, 0, 0);
|
||||
const startedAt = base.toISOString();
|
||||
s.db.prepare('UPDATE runs SET started_at = ? WHERE id = ?').run(startedAt, r1.id);
|
||||
|
||||
const utcDay = startedAt.slice(0, 10);
|
||||
const ld = new Date(startedAt);
|
||||
const localDay = `${ld.getFullYear()}-${String(ld.getMonth() + 1).padStart(2, '0')}-${String(ld.getDate()).padStart(2, '0')}`;
|
||||
assert.notEqual(localDay, utcDay, 'UTC+8 下 18:00Z 应跨到本地次日,前提才成立');
|
||||
|
||||
const detail = s.usageDetail('day');
|
||||
assert.ok(detail.series.some((b) => b.bucket === localDay), `应分桶到本地日 ${localDay}`);
|
||||
assert.equal(detail.series.some((b) => b.bucket === utcDay), false, `不应再用 UTC 日 ${utcDay}`);
|
||||
|
||||
// —— 当天窗口:本地今天 02:00(UTC 仍为昨天 18:00)的 run 应计入「今天」 ——
|
||||
const r2 = s.startRun(t.id, 'executor');
|
||||
s.setRunUsage(r2.id, { input: 0, output: 1_000_000, cacheRead: 0, cacheWrite: 0 }, 'claude-opus-4-8'); // $75
|
||||
const today2am = new Date();
|
||||
today2am.setHours(2, 0, 0, 0); // 本地今天 02:00
|
||||
s.db.prepare('UPDATE runs SET started_at = ? WHERE id = ?').run(today2am.toISOString(), r2.id);
|
||||
assert.ok(s.costSummary('day').total >= 75, '本地 00:00–08:00 的 run 应计入今天(旧 UTC 口径会漏算)');
|
||||
|
||||
s.close();
|
||||
} finally {
|
||||
if (origTZ === undefined) delete process.env.TZ; else process.env.TZ = origTZ;
|
||||
}
|
||||
});
|
||||
|
||||
test('store: enforceBudget 超额 → 暂停项目 + 广播 budget.exceeded', () => {
|
||||
const s = new Store(':memory:');
|
||||
const p = s.createProject({ name: 'b', repoPath: '/tmp/b-' + Math.random() });
|
||||
|
||||
Reference in New Issue
Block a user