Commit e287643cbf352cd115101b5ec9a3aff8e65e9ee3
1 parent
15e3db57
perf: fix critical I/O bottlenecks - cache /api/agent/list (5s TTL), buffer log …
…writes (4KB/1s), remove messages.log fallback in loadHistory
Showing
1 changed file
with
54 additions
and
23 deletions
server/routes/agent-chat.js
| ... | ... | @@ -493,28 +493,10 @@ function saveMeta(agentId, convId, meta) { |
| 493 | 493 | function loadHistory(agentId, convId) { |
| 494 | 494 | try { |
| 495 | 495 | const historyPath = path.join(getAgentDir(agentId), 'conversations', convId, 'history.json'); |
| 496 | - const logPath = path.join(getAgentDir(agentId), 'conversations', convId, 'messages.log'); | |
| 497 | - | |
| 498 | - let history = []; | |
| 499 | 496 | if (fs.existsSync(historyPath)) { |
| 500 | - history = JSON.parse(fs.readFileSync(historyPath, 'utf-8')); | |
| 501 | - } | |
| 502 | - | |
| 503 | - // 如果 messages.log 存在且比 history.json 更丰富, 从 log 里解析完整消息 | |
| 504 | - if (fs.existsSync(logPath)) { | |
| 505 | - const logContent = fs.readFileSync(logPath, 'utf-8'); | |
| 506 | - const parsed = parseMessagesLog(logContent); | |
| 507 | - // 只有当 log 解析出的消息更丰富时才使用 (例如包含更多内容) | |
| 508 | - if (parsed.length > 0) { | |
| 509 | - const historyTotalLen = history.reduce((sum, m) => sum + (m.content || '').length, 0); | |
| 510 | - const logTotalLen = parsed.reduce((sum, m) => sum + (m.content || '').length, 0); | |
| 511 | - if (logTotalLen > historyTotalLen * 1.5) { | |
| 512 | - return parsed; | |
| 513 | - } | |
| 514 | - } | |
| 497 | + return JSON.parse(fs.readFileSync(historyPath, 'utf-8')); | |
| 515 | 498 | } |
| 516 | - | |
| 517 | - return history; | |
| 499 | + return []; | |
| 518 | 500 | } catch { return []; } |
| 519 | 501 | } |
| 520 | 502 | |
| ... | ... | @@ -552,10 +534,43 @@ function appendHistory(agentId, convId, msg) { |
| 552 | 534 | fs.writeFileSync(path.join(dir, 'history.json'), JSON.stringify(history, null, 2)); |
| 553 | 535 | } |
| 554 | 536 | |
| 555 | -/** 写日志(流式或单次) */ | |
| 537 | +/** 写日志(流式,带缓冲) */ | |
| 538 | +const _logBuffers = new Map(); | |
| 539 | +const LOG_BUFFER_SIZE = 4096; // 4KB | |
| 540 | +const LOG_FLUSH_INTERVAL = 1000; // 1s | |
| 541 | + | |
| 556 | 542 | function logMessageChunk(agentId, convId, content) { |
| 557 | - const dir = getConvDir(agentId, convId); | |
| 558 | - fs.appendFileSync(path.join(dir, 'messages.log'), content, 'utf-8'); | |
| 543 | + const key = `${agentId}::${convId}`; | |
| 544 | + if (!_logBuffers.has(key)) { | |
| 545 | + _logBuffers.set(key, { buf: '', timer: null, agentId, convId }); | |
| 546 | + } | |
| 547 | + const entry = _logBuffers.get(key); | |
| 548 | + entry.buf += content; | |
| 549 | + if (entry.buf.length >= LOG_BUFFER_SIZE) { | |
| 550 | + _flushLogBuffer(key); | |
| 551 | + } else if (!entry.timer) { | |
| 552 | + entry.timer = setTimeout(() => _flushLogBuffer(key), LOG_FLUSH_INTERVAL); | |
| 553 | + } | |
| 554 | +} | |
| 555 | + | |
| 556 | +function _flushLogBuffer(key) { | |
| 557 | + const entry = _logBuffers.get(key); | |
| 558 | + if (!entry || !entry.buf) return; | |
| 559 | + try { | |
| 560 | + const dir = getConvDir(entry.agentId, entry.convId); | |
| 561 | + fs.appendFileSync(path.join(dir, 'messages.log'), entry.buf, 'utf-8'); | |
| 562 | + } catch (err) { | |
| 563 | + console.error(`[LogBuffer] ⚠️ flush 失败: ${key}`, err.message); | |
| 564 | + } | |
| 565 | + entry.buf = ''; | |
| 566 | + if (entry.timer) { clearTimeout(entry.timer); entry.timer = null; } | |
| 567 | +} | |
| 568 | + | |
| 569 | +/** 强制刷出指定对话的日志缓冲 */ | |
| 570 | +function flushLogBufferForConv(agentId, convId) { | |
| 571 | + const key = `${agentId}::${convId}`; | |
| 572 | + _flushLogBuffer(key); | |
| 573 | + _logBuffers.delete(key); | |
| 559 | 574 | } |
| 560 | 575 | |
| 561 | 576 | /** 传统单次写日志 (为了兼容) */ |
| ... | ... | @@ -818,8 +833,18 @@ router.get('/quota', (req, res) => { |
| 818 | 833 | } |
| 819 | 834 | }); |
| 820 | 835 | |
| 836 | +// ── Agent List 缓存 (5秒 TTL,避免每次请求全量扫描磁盘) ── | |
| 837 | +let _agentListCache = null; | |
| 838 | +let _agentListCacheTime = 0; | |
| 839 | +const AGENT_LIST_CACHE_TTL = 5000; // 5 秒 | |
| 840 | + | |
| 821 | 841 | router.get('/list', (req, res) => { |
| 822 | 842 | try { |
| 843 | + // 缓存命中:5秒内直接返回 | |
| 844 | + if (_agentListCache && Date.now() - _agentListCacheTime < AGENT_LIST_CACHE_TTL) { | |
| 845 | + return res.json(_agentListCache); | |
| 846 | + } | |
| 847 | + | |
| 823 | 848 | const allAgents = [] |
| 824 | 849 | const DATA_ROOT = path.resolve(__dirname, '../../data') |
| 825 | 850 | |
| ... | ... | @@ -927,6 +952,10 @@ router.get('/list', (req, res) => { |
| 927 | 952 | return tb - ta |
| 928 | 953 | }) |
| 929 | 954 | |
| 955 | + // 更新缓存 | |
| 956 | + _agentListCache = allAgents; | |
| 957 | + _agentListCacheTime = Date.now(); | |
| 958 | + | |
| 930 | 959 | res.json(allAgents) |
| 931 | 960 | } catch (err) { res.status(500).json({ error: err.message }) } |
| 932 | 961 | }); |
| ... | ... | @@ -2361,6 +2390,7 @@ ${resultsText.join('\n\n')} |
| 2361 | 2390 | } |
| 2362 | 2391 | |
| 2363 | 2392 | meta.status = 'completed'; |
| 2393 | + flushLogBufferForConv(agentId, convId); // 刷出日志缓冲 | |
| 2364 | 2394 | saveMeta(agentId, convId, meta); |
| 2365 | 2395 | |
| 2366 | 2396 | // ── Phase 1 MVP: 自动保存 Session 摘要到记忆 ── |
| ... | ... | @@ -2382,6 +2412,7 @@ ${resultsText.join('\n\n')} |
| 2382 | 2412 | } catch (err) { |
| 2383 | 2413 | console.error('[AgentChat] Error:', err.message); |
| 2384 | 2414 | logMessageChunk(agentId, convId, `\n\n[ERROR]\n${err.message}\n\n`); |
| 2415 | + flushLogBufferForConv(agentId, convId); // 刷出日志缓冲 | |
| 2385 | 2416 | meta.status = 'failed'; |
| 2386 | 2417 | meta.error = err.message; |
| 2387 | 2418 | saveMeta(agentId, convId, meta); | ... | ... |