fix(usage): 修按天分桶时区错位 + 区分账号额度/maestro用量口径 + 成本说明文档 (tsk_JKAzHuXvSw7h)
全局面板「用量」数据问题修复与澄清: 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>
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',
|
||||
|
||||
Reference in New Issue
Block a user