cc-viewer 1.6.32 → 1.6.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js CHANGED
@@ -43,9 +43,10 @@ import { uploadPlugins, installPluginFromUrl } from './lib/plugin-manager.js';
43
43
  import { getUserProfile } from './lib/user-profile.js';
44
44
  import { getGitDiffs } from './lib/git-diff.js';
45
45
  import { CONTEXT_WINDOW_FILE, readModelContextSize, buildContextWindowEvent, getContextSizeForModel } from './lib/context-watcher.js';
46
- import { readLogFile, watchLogFile, startWatching, getWatchedFiles } from './lib/log-watcher.js';
46
+ import { watchLogFile, startWatching, getWatchedFiles } from './lib/log-watcher.js';
47
47
  import { isMainAgentEntry, extractCachedContent } from './lib/kv-cache-analyzer.js';
48
- import { listLocalLogs, readLocalLog, deleteLogFiles, mergeLogFiles } from './lib/log-management.js';
48
+ import { listLocalLogs, deleteLogFiles, mergeLogFiles } from './lib/log-management.js';
49
+ import { countLogEntries, streamRawEntriesAsync } from './lib/log-stream.js';
49
50
  import { detectTargetLang, translate } from './lib/translator.js';
50
51
 
51
52
  const PREFS_FILE = join(LOG_DIR, 'preferences.json');
@@ -348,7 +349,7 @@ async function handleRequest(req, res) {
348
349
  if (url === '/api/resume-choice' && method === 'POST') {
349
350
  let body = '';
350
351
  req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
351
- req.on('end', () => {
352
+ req.on('end', async () => {
352
353
  try {
353
354
  const { choice } = JSON.parse(body);
354
355
  if (choice !== 'continue' && choice !== 'new') {
@@ -371,12 +372,18 @@ async function handleRequest(req, res) {
371
372
  client.write(`event: resume_resolved\ndata: ${resolvedData}\n\n`);
372
373
  } catch { }
373
374
  });
374
- // 发送 full_reload 让客户端重新加载数据
375
- const entries = readLogFile(LOG_FILE);
375
+ // 流式分段广播 full_reload,避免全量加载 OOM
376
+ const reloadTotal = countLogEntries(LOG_FILE);
376
377
  clients.forEach(client => {
377
- try {
378
- client.write(`event: full_reload\ndata: ${JSON.stringify(entries)}\n\n`);
379
- } catch { }
378
+ try { client.write(`event: load_start\ndata: ${JSON.stringify({ total: reloadTotal, incremental: false })}\n\n`); } catch { }
379
+ });
380
+ await streamRawEntriesAsync(LOG_FILE, (raw) => {
381
+ clients.forEach(client => {
382
+ try { client.write('event: load_chunk\ndata: ['); client.write(raw); client.write(']\n\n'); } catch { }
383
+ });
384
+ });
385
+ clients.forEach(client => {
386
+ try { client.write(`event: load_end\ndata: {}\n\n`); } catch { }
380
387
  });
381
388
  res.writeHead(200, { 'Content-Type': 'application/json' });
382
389
  res.end(JSON.stringify({ ok: true, logFile: result.logFile }));
@@ -528,12 +535,18 @@ async function handleRequest(req, res) {
528
535
  } catch {}
529
536
  });
530
537
 
531
- // 发送 full_reload 以刷新会话区域
532
- const entries = readLogFile(LOG_FILE);
538
+ // 流式分段广播以刷新会话区域,避免全量加载 OOM
539
+ const wsReloadTotal = countLogEntries(LOG_FILE);
533
540
  clients.forEach(client => {
534
- try {
535
- client.write(`event: full_reload\ndata: ${JSON.stringify(entries)}\n\n`);
536
- } catch {}
541
+ try { client.write(`event: load_start\ndata: ${JSON.stringify({ total: wsReloadTotal, incremental: false })}\n\n`); } catch {}
542
+ });
543
+ await streamRawEntriesAsync(LOG_FILE, (raw) => {
544
+ clients.forEach(client => {
545
+ try { client.write('event: load_chunk\ndata: ['); client.write(raw); client.write(']\n\n'); } catch {}
546
+ });
547
+ });
548
+ clients.forEach(client => {
549
+ try { client.write(`event: load_end\ndata: {}\n\n`); } catch {}
537
550
  });
538
551
 
539
552
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -632,62 +645,48 @@ async function handleRequest(req, res) {
632
645
  res.write(`event: resume_prompt\ndata: ${JSON.stringify({ recentFileName: _resumeState.recentFileName })}\n\n`);
633
646
  }
634
647
 
635
- const entries = readLogFile(LOG_FILE);
636
- // 增量加载:客户端传 since(最后条目时间戳)和 cc(缓存条目数)
637
- const since = parsedUrl.searchParams.get('since');
638
- const cc = parseInt(parsedUrl.searchParams.get('cc') || '0', 10);
639
- let entriesToSend = entries;
640
- let incremental = false;
641
- if (since && cc > 0) {
642
- const sinceMs = new Date(since).getTime();
643
- if (!isNaN(sinceMs)) {
644
- const delta = entries.filter(e => e.timestamp && new Date(e.timestamp).getTime() > sinceMs);
645
- if (cc + delta.length === entries.length) {
646
- entriesToSend = delta;
647
- incremental = true;
648
- }
649
- }
650
- }
651
- // 分段发送:先告知总数,再分块传输,让前端能显示真实加载进度
652
- const CHUNK_SIZE = 50;
653
- if (entriesToSend.length > CHUNK_SIZE) {
654
- res.write(`event: load_start\ndata: ${JSON.stringify({ total: entriesToSend.length, incremental })}\n\n`);
655
- for (let i = 0; i < entriesToSend.length; i += CHUNK_SIZE) {
656
- const chunk = entriesToSend.slice(i, i + CHUNK_SIZE);
657
- res.write(`event: load_chunk\ndata: ${JSON.stringify(chunk)}\n\n`);
658
- }
659
- res.write(`event: load_end\ndata: {}\n\n`);
660
- } else if (incremental) {
661
- // 增量模式:即使条目少也走 load_start/load_end 流程(可能 0 条新数据)
662
- res.write(`event: load_start\ndata: ${JSON.stringify({ total: entriesToSend.length, incremental: true })}\n\n`);
663
- if (entriesToSend.length > 0) {
664
- res.write(`event: load_chunk\ndata: ${JSON.stringify(entriesToSend)}\n\n`);
665
- }
666
- res.write(`event: load_end\ndata: {}\n\n`);
667
- } else {
668
- res.write(`event: full_reload\ndata: ${JSON.stringify(entriesToSend)}\n\n`);
669
- }
648
+ // 流式发送原始 delta 条目,客户端自行重建(避免 server OOM)
649
+ // 注:streamRawEntriesAsync 不支持 since 过滤,始终发送全量数据
650
+ const total = countLogEntries(LOG_FILE);
651
+ res.write(`event: load_start\ndata: ${JSON.stringify({ total, incremental: false })}\n\n`);
670
652
 
671
- // Compute KV-Cache content + context_window for latest MainAgent
653
+ // 流式分段发送 + 追踪最新 MainAgent 的 KV-Cache context_window
654
+ let latestKvCache = null;
655
+ let latestContextWindow = null;
672
656
  let pushedContextWindow = false;
673
- for (let i = entries.length - 1; i >= 0; i--) {
674
- if (isMainAgentEntry(entries[i])) {
675
- const cached = extractCachedContent(entries[i]);
676
- if (cached) {
677
- res.write(`event: kv_cache_content\ndata: ${JSON.stringify(cached)}\n\n`);
678
- }
679
- // Push initial context_window from latest MainAgent usage
680
- const usage = entries[i].response?.body?.usage;
681
- if (usage) {
682
- const contextSize = getContextSizeForModel(entries[i].body?.model);
683
- const cwData = buildContextWindowEvent(usage, contextSize);
684
- if (cwData) {
685
- res.write(`event: context_window\ndata: ${JSON.stringify(cwData)}\n\n`);
686
- pushedContextWindow = true;
657
+
658
+ await streamRawEntriesAsync(LOG_FILE, (raw) => {
659
+ // 直接发送原始 JSON 字符串,不做 parse/reconstruct/stringify
660
+ res.write('event: load_chunk\ndata: [');
661
+ res.write(raw);
662
+ res.write(']\n\n');
663
+ // 轻量追踪最新 MainAgent KV-Cache context_window(仅 regex 检测)
664
+ if (raw.includes('"mainAgent":true') || raw.includes('"mainAgent": true')) {
665
+ try {
666
+ const entry = JSON.parse(raw);
667
+ if (isMainAgentEntry(entry)) {
668
+ const cached = extractCachedContent(entry);
669
+ if (cached) latestKvCache = cached;
670
+ const usage = entry.response?.body?.usage;
671
+ if (usage) {
672
+ const contextSize = getContextSizeForModel(entry.body?.model);
673
+ const cw = buildContextWindowEvent(usage, contextSize);
674
+ if (cw) latestContextWindow = cw;
675
+ }
687
676
  }
688
- }
689
- break;
677
+ } catch { }
690
678
  }
679
+ });
680
+
681
+ res.write(`event: load_end\ndata: {}\n\n`);
682
+
683
+ // 发送最新 MainAgent 的 KV-Cache 和 context_window
684
+ if (latestKvCache) {
685
+ res.write(`event: kv_cache_content\ndata: ${JSON.stringify(latestKvCache)}\n\n`);
686
+ }
687
+ if (latestContextWindow) {
688
+ res.write(`event: context_window\ndata: ${JSON.stringify(latestContextWindow)}\n\n`);
689
+ pushedContextWindow = true;
691
690
  }
692
691
  // Fallback: no MainAgent in log (e.g. fresh session after -c), read context-window.json
693
692
  if (!pushedContextWindow) {
@@ -718,9 +717,17 @@ async function handleRequest(req, res) {
718
717
 
719
718
  // API endpoint
720
719
  if (url === '/api/requests' && method === 'GET') {
721
- const entries = readLogFile(LOG_FILE);
720
+ // 异步流式 JSON 数组输出,不做 reconstruct,发原始条目
722
721
  res.writeHead(200, { 'Content-Type': 'application/json' });
723
- res.end(JSON.stringify(entries));
722
+ res.write('[');
723
+ let first = true;
724
+ await streamRawEntriesAsync(LOG_FILE, (raw) => {
725
+ if (!first) res.write(',');
726
+ res.write(raw);
727
+ first = false;
728
+ });
729
+ res.write(']');
730
+ res.end();
724
731
  return;
725
732
  }
726
733
 
@@ -1325,24 +1332,17 @@ async function handleRequest(req, res) {
1325
1332
  const stream = createReadStream(realPath);
1326
1333
  stream.pipe(res);
1327
1334
  } else {
1328
- // 重建为全量格式下载
1329
- const { readLocalLog } = await import('./lib/log-management.js');
1330
- const entries = readLocalLog(LOG_DIR, file);
1331
- // 清除 delta 元字段
1332
- for (const entry of entries) {
1333
- delete entry._deltaFormat;
1334
- delete entry._totalMessageCount;
1335
- delete entry._conversationId;
1336
- delete entry._isCheckpoint;
1337
- }
1338
- const content = entries.map(e => JSON.stringify(e)).join('\n---\n') + '\n---\n';
1339
- const buf = Buffer.from(content, 'utf-8');
1335
+ // 流式下载原始条目(不重建,保持 delta 格式),避免 OOM
1340
1336
  res.writeHead(200, {
1341
1337
  'Content-Type': 'application/octet-stream',
1342
1338
  'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
1343
- 'Content-Length': buf.length,
1339
+ 'Transfer-Encoding': 'chunked',
1344
1340
  });
1345
- res.end(buf);
1341
+ await streamRawEntriesAsync(realPath, (raw) => {
1342
+ res.write(raw);
1343
+ res.write('\n---\n');
1344
+ });
1345
+ res.end();
1346
1346
  }
1347
1347
  } catch (err) {
1348
1348
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1368,13 +1368,35 @@ async function handleRequest(req, res) {
1368
1368
  }
1369
1369
 
1370
1370
  try {
1371
- const entries = readLocalLog(LOG_DIR, file);
1372
- res.writeHead(200, { 'Content-Type': 'application/json' });
1373
- res.end(JSON.stringify(entries));
1371
+ // 独立 SSE 流:直接向请求方返回 event-stream,不走 /events 广播
1372
+ const { validateLogPath } = await import('./lib/log-management.js');
1373
+ validateLogPath(LOG_DIR, file);
1374
+ const filePath = join(LOG_DIR, file);
1375
+ const total = countLogEntries(filePath);
1376
+
1377
+ res.writeHead(200, {
1378
+ 'Content-Type': 'text/event-stream',
1379
+ 'Cache-Control': 'no-cache',
1380
+ 'Connection': 'keep-alive',
1381
+ });
1382
+
1383
+ res.write(`event: load_start\ndata: ${JSON.stringify({ total, incremental: false })}\n\n`);
1384
+ await streamRawEntriesAsync(filePath, (raw) => {
1385
+ res.write('event: load_chunk\ndata: [');
1386
+ res.write(raw);
1387
+ res.write(']\n\n');
1388
+ });
1389
+ res.write(`event: load_end\ndata: {}\n\n`);
1390
+ res.end();
1374
1391
  } catch (err) {
1375
- const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'ACCESS_DENIED' ? 403 : 500;
1376
- res.writeHead(status, { 'Content-Type': 'application/json' });
1377
- res.end(JSON.stringify({ error: err.message }));
1392
+ // 如果 headers 未发送,返回 JSON 错误;否则关闭连接
1393
+ if (!res.headersSent) {
1394
+ const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'ACCESS_DENIED' ? 403 : 500;
1395
+ res.writeHead(status, { 'Content-Type': 'application/json' });
1396
+ res.end(JSON.stringify({ error: err.message }));
1397
+ } else {
1398
+ res.end();
1399
+ }
1378
1400
  }
1379
1401
  return;
1380
1402
  }