From a50f59d5153a2b98845378b4ced1829c5e7ee582 Mon Sep 17 00:00:00 2001 From: yibo Date: Wed, 1 Apr 2026 11:08:00 +0800 Subject: [PATCH] perf: billing.log date-sharding - write to billing-YYYY-MM-DD.log, read with 30-day window, skip old files --- server/routes/agent-chat.js | 57 ++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/server/routes/agent-chat.js b/server/routes/agent-chat.js index 9b24480..2ae7298 100644 --- a/server/routes/agent-chat.js +++ b/server/routes/agent-chat.js @@ -791,28 +791,42 @@ router.get('/quota', (req, res) => { try { const DATA_ROOT = path.resolve(__dirname, '../../data'); const records = []; + const daysBack = parseInt(String(req.query.days || '30')) || 30; // 默认读最近 30 天 + const cutoff = new Date(Date.now() - daysBack * 86400000).toISOString(); - // 读取所有 billing.log + // 读取 billing 日志(兼容旧 billing.log + 新 billing-YYYY-MM-DD.log) const readBilling = (dir) => { - const p = path.join(dir, 'billing.log'); - if (fs.existsSync(p)) { - fs.readFileSync(p, 'utf-8').split('\n').forEach(line => { - line = line.trim(); - if (line.startsWith('{')) { - try { records.push(JSON.parse(line)); } catch(e) {} - } else if (line) { - // 旧的纯文本兼容: 2026-03-28T18:00:00.000Z | Agent: master | Conv: 123 | Cost: $0.123 | Duration: 123ms - try { - const parts = line.split('|').map(p => p.trim()); - const ts = parts[0]; - const ai = parts[1].replace('Agent:', '').trim(); - const ci = parts[2].replace('Conv:', '').trim(); - const cost = parseFloat(parts[3].replace('Cost: $', '').trim()); - const dur = parseInt(parts[4].replace('Duration:', '').replace('ms', '').trim()); - records.push({ timestamp: ts, agentId: ai, convId: ci, costUsd: cost || 0, durationMs: dur || 0, usage: { input: 0, output: 0 } }); - } catch(e) {} - } - }); + if (!fs.existsSync(dir)) return; + const files = fs.readdirSync(dir).filter(f => f.startsWith('billing')); + for (const file of files) { + // 按日分片优化:跳过超出时间范围的文件 + const dateMatch = file.match(/billing-(\d{4}-\d{2}-\d{2})\.log/); + if (dateMatch && dateMatch[1] < cutoff.substring(0, 10)) continue; + + const p = path.join(dir, file); + try { + fs.readFileSync(p, 'utf-8').split('\n').forEach(line => { + line = line.trim(); + if (line.startsWith('{')) { + try { + const rec = JSON.parse(line); + if (rec.timestamp >= cutoff) records.push(rec); + } catch(e) {} + } else if (line) { + // 旧的纯文本兼容 + try { + const parts = line.split('|').map(p => p.trim()); + const ts = parts[0]; + if (ts < cutoff) return; + const ai = parts[1].replace('Agent:', '').trim(); + const ci = parts[2].replace('Conv:', '').trim(); + const cost = parseFloat(parts[3].replace('Cost: $', '').trim()); + const dur = parseInt(parts[4].replace('Duration:', '').replace('ms', '').trim()); + records.push({ timestamp: ts, agentId: ai, convId: ci, costUsd: cost || 0, durationMs: dur || 0, usage: { input: 0, output: 0 } }); + } catch(e) {} + } + }); + } catch(e) {} } }; @@ -2226,7 +2240,8 @@ router.post('/:agentId/chat', async (req, res) => { durationMs: event.params.duration_ms, usage: event.params.usage || { input: 0, output: 0 } }; - fs.appendFileSync(path.join(billingDir, 'billing.log'), JSON.stringify(logEntry) + '\n', 'utf-8'); + const billingDate = new Date().toISOString().substring(0, 10); + fs.appendFileSync(path.join(billingDir, `billing-${billingDate}.log`), JSON.stringify(logEntry) + '\n', 'utf-8'); } catch (err) {} } if (!fullOutput && event.params?.result) { -- libgit2 0.21.4