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/dist/assets/{index-BlNBvL5K.css → index-DjR98uUB.css} +1 -1
- package/dist/assets/{index-AD0x3eII.js → index-qJnP3J3V.js} +139 -139
- package/dist/index.html +2 -2
- package/interceptor.js +2 -2
- package/lib/delta-reconstructor.js +75 -3
- package/lib/log-management.js +20 -24
- package/lib/log-stream.js +192 -0
- package/lib/log-watcher.js +25 -8
- package/package.json +1 -1
- package/server.js +109 -87
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 {
|
|
46
|
+
import { watchLogFile, startWatching, getWatchedFiles } from './lib/log-watcher.js';
|
|
47
47
|
import { isMainAgentEntry, extractCachedContent } from './lib/kv-cache-analyzer.js';
|
|
48
|
-
import { listLocalLogs,
|
|
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
|
-
//
|
|
375
|
-
const
|
|
375
|
+
// 流式分段广播 full_reload,避免全量加载 OOM
|
|
376
|
+
const reloadTotal = countLogEntries(LOG_FILE);
|
|
376
377
|
clients.forEach(client => {
|
|
377
|
-
try {
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
//
|
|
532
|
-
const
|
|
538
|
+
// 流式分段广播以刷新会话区域,避免全量加载 OOM
|
|
539
|
+
const wsReloadTotal = countLogEntries(LOG_FILE);
|
|
533
540
|
clients.forEach(client => {
|
|
534
|
-
try {
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
636
|
-
//
|
|
637
|
-
const
|
|
638
|
-
|
|
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
|
-
//
|
|
653
|
+
// 流式分段发送 + 追踪最新 MainAgent 的 KV-Cache 和 context_window
|
|
654
|
+
let latestKvCache = null;
|
|
655
|
+
let latestContextWindow = null;
|
|
672
656
|
let pushedContextWindow = false;
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
720
|
+
// 异步流式 JSON 数组输出,不做 reconstruct,发原始条目
|
|
722
721
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
723
|
-
res.
|
|
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
|
-
'
|
|
1339
|
+
'Transfer-Encoding': 'chunked',
|
|
1344
1340
|
});
|
|
1345
|
-
|
|
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
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
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
|
-
|
|
1376
|
-
res.
|
|
1377
|
-
|
|
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
|
}
|