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:
@@ -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