cc-viewer 1.6.32 → 1.6.34

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,10 +43,11 @@ 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';
49
- import { detectTargetLang, translate } from './lib/translator.js';
48
+ import { listLocalLogs, deleteLogFiles, mergeLogFiles } from './lib/log-management.js';
49
+ import { countLogEntries, streamRawEntriesAsync } from './lib/log-stream.js';
50
+
50
51
 
51
52
  const PREFS_FILE = join(LOG_DIR, 'preferences.json');
52
53
 
@@ -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 }));
@@ -388,58 +395,6 @@ async function handleRequest(req, res) {
388
395
  return;
389
396
  }
390
397
 
391
- // 翻译 API
392
- if (url === '/api/translate' && method === 'POST') {
393
- let body = '';
394
- req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
395
- req.on('end', async () => {
396
- try {
397
- const { text, from = 'en', to } = JSON.parse(body);
398
- if (!text) {
399
- res.writeHead(400, { 'Content-Type': 'application/json' });
400
- res.end(JSON.stringify({ error: 'Missing "text" field' }));
401
- return;
402
- }
403
-
404
- // 确定目标语言
405
- const targetLang = to || detectTargetLang(PREFS_FILE);
406
-
407
- // 获取 API Key(仅 x-api-key 认证,不复用 session token 避免上下文污染)
408
- // 优先级: 环境变量 > 拦截缓存 > 从 authHeader 中提取 sk- 开头的 key
409
- let apiKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || _cachedApiKey;
410
- if (!apiKey && _cachedAuthHeader) {
411
- const m = _cachedAuthHeader.match(/^Bearer\s+(sk-\S+)$/i);
412
- if (m) apiKey = m[1];
413
- }
414
- if (!apiKey) {
415
- res.writeHead(501, { 'Content-Type': 'application/json' });
416
- res.end(JSON.stringify({ error: 'No API key available. Set ANTHROPIC_API_KEY or use x-api-key authentication.' }));
417
- return;
418
- }
419
-
420
- const result = await translate({
421
- text,
422
- from,
423
- to: targetLang,
424
- apiKey,
425
- baseUrl: process.env.ANTHROPIC_BASE_URL,
426
- model: _cachedHaikuModel,
427
- });
428
-
429
- res.writeHead(200, { 'Content-Type': 'application/json' });
430
- res.end(JSON.stringify(result));
431
- } catch (err) {
432
- const status = err.status ? 502 : 500;
433
- const payload = err.status
434
- ? { error: 'Translation API failed', status: err.status, detail: err.detail }
435
- : { error: 'Internal error', message: err.message };
436
- res.writeHead(status, { 'Content-Type': 'application/json' });
437
- res.end(JSON.stringify(payload));
438
- }
439
- });
440
- return;
441
- }
442
-
443
398
  // === Workspace API ===
444
399
 
445
400
  // 目录浏览器
@@ -528,12 +483,18 @@ async function handleRequest(req, res) {
528
483
  } catch {}
529
484
  });
530
485
 
531
- // 发送 full_reload 以刷新会话区域
532
- const entries = readLogFile(LOG_FILE);
486
+ // 流式分段广播以刷新会话区域,避免全量加载 OOM
487
+ const wsReloadTotal = countLogEntries(LOG_FILE);
533
488
  clients.forEach(client => {
534
- try {
535
- client.write(`event: full_reload\ndata: ${JSON.stringify(entries)}\n\n`);
536
- } catch {}
489
+ try { client.write(`event: load_start\ndata: ${JSON.stringify({ total: wsReloadTotal, incremental: false })}\n\n`); } catch {}
490
+ });
491
+ await streamRawEntriesAsync(LOG_FILE, (raw) => {
492
+ clients.forEach(client => {
493
+ try { client.write('event: load_chunk\ndata: ['); client.write(raw); client.write(']\n\n'); } catch {}
494
+ });
495
+ });
496
+ clients.forEach(client => {
497
+ try { client.write(`event: load_end\ndata: {}\n\n`); } catch {}
537
498
  });
538
499
 
539
500
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -632,62 +593,48 @@ async function handleRequest(req, res) {
632
593
  res.write(`event: resume_prompt\ndata: ${JSON.stringify({ recentFileName: _resumeState.recentFileName })}\n\n`);
633
594
  }
634
595
 
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
- }
596
+ // 流式发送原始 delta 条目,客户端自行重建(避免 server OOM)
597
+ // 注:streamRawEntriesAsync 不支持 since 过滤,始终发送全量数据
598
+ const total = countLogEntries(LOG_FILE);
599
+ res.write(`event: load_start\ndata: ${JSON.stringify({ total, incremental: false })}\n\n`);
670
600
 
671
- // Compute KV-Cache content + context_window for latest MainAgent
601
+ // 流式分段发送 + 追踪最新 MainAgent 的 KV-Cache context_window
602
+ let latestKvCache = null;
603
+ let latestContextWindow = null;
672
604
  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;
605
+
606
+ await streamRawEntriesAsync(LOG_FILE, (raw) => {
607
+ // 直接发送原始 JSON 字符串,不做 parse/reconstruct/stringify
608
+ res.write('event: load_chunk\ndata: [');
609
+ res.write(raw);
610
+ res.write(']\n\n');
611
+ // 轻量追踪最新 MainAgent KV-Cache context_window(仅 regex 检测)
612
+ if (raw.includes('"mainAgent":true') || raw.includes('"mainAgent": true')) {
613
+ try {
614
+ const entry = JSON.parse(raw);
615
+ if (isMainAgentEntry(entry)) {
616
+ const cached = extractCachedContent(entry);
617
+ if (cached) latestKvCache = cached;
618
+ const usage = entry.response?.body?.usage;
619
+ if (usage) {
620
+ const contextSize = getContextSizeForModel(entry.body?.model);
621
+ const cw = buildContextWindowEvent(usage, contextSize);
622
+ if (cw) latestContextWindow = cw;
623
+ }
687
624
  }
688
- }
689
- break;
625
+ } catch { }
690
626
  }
627
+ });
628
+
629
+ res.write(`event: load_end\ndata: {}\n\n`);
630
+
631
+ // 发送最新 MainAgent 的 KV-Cache 和 context_window
632
+ if (latestKvCache) {
633
+ res.write(`event: kv_cache_content\ndata: ${JSON.stringify(latestKvCache)}\n\n`);
634
+ }
635
+ if (latestContextWindow) {
636
+ res.write(`event: context_window\ndata: ${JSON.stringify(latestContextWindow)}\n\n`);
637
+ pushedContextWindow = true;
691
638
  }
692
639
  // Fallback: no MainAgent in log (e.g. fresh session after -c), read context-window.json
693
640
  if (!pushedContextWindow) {
@@ -718,9 +665,17 @@ async function handleRequest(req, res) {
718
665
 
719
666
  // API endpoint
720
667
  if (url === '/api/requests' && method === 'GET') {
721
- const entries = readLogFile(LOG_FILE);
668
+ // 异步流式 JSON 数组输出,不做 reconstruct,发原始条目
722
669
  res.writeHead(200, { 'Content-Type': 'application/json' });
723
- res.end(JSON.stringify(entries));
670
+ res.write('[');
671
+ let first = true;
672
+ await streamRawEntriesAsync(LOG_FILE, (raw) => {
673
+ if (!first) res.write(',');
674
+ res.write(raw);
675
+ first = false;
676
+ });
677
+ res.write(']');
678
+ res.end();
724
679
  return;
725
680
  }
726
681
 
@@ -1325,24 +1280,17 @@ async function handleRequest(req, res) {
1325
1280
  const stream = createReadStream(realPath);
1326
1281
  stream.pipe(res);
1327
1282
  } 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');
1283
+ // 流式下载原始条目(不重建,保持 delta 格式),避免 OOM
1340
1284
  res.writeHead(200, {
1341
1285
  'Content-Type': 'application/octet-stream',
1342
1286
  'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
1343
- 'Content-Length': buf.length,
1287
+ 'Transfer-Encoding': 'chunked',
1288
+ });
1289
+ await streamRawEntriesAsync(realPath, (raw) => {
1290
+ res.write(raw);
1291
+ res.write('\n---\n');
1344
1292
  });
1345
- res.end(buf);
1293
+ res.end();
1346
1294
  }
1347
1295
  } catch (err) {
1348
1296
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -1368,13 +1316,35 @@ async function handleRequest(req, res) {
1368
1316
  }
1369
1317
 
1370
1318
  try {
1371
- const entries = readLocalLog(LOG_DIR, file);
1372
- res.writeHead(200, { 'Content-Type': 'application/json' });
1373
- res.end(JSON.stringify(entries));
1319
+ // 独立 SSE 流:直接向请求方返回 event-stream,不走 /events 广播
1320
+ const { validateLogPath } = await import('./lib/log-management.js');
1321
+ validateLogPath(LOG_DIR, file);
1322
+ const filePath = join(LOG_DIR, file);
1323
+ const total = countLogEntries(filePath);
1324
+
1325
+ res.writeHead(200, {
1326
+ 'Content-Type': 'text/event-stream',
1327
+ 'Cache-Control': 'no-cache',
1328
+ 'Connection': 'keep-alive',
1329
+ });
1330
+
1331
+ res.write(`event: load_start\ndata: ${JSON.stringify({ total, incremental: false })}\n\n`);
1332
+ await streamRawEntriesAsync(filePath, (raw) => {
1333
+ res.write('event: load_chunk\ndata: [');
1334
+ res.write(raw);
1335
+ res.write(']\n\n');
1336
+ });
1337
+ res.write(`event: load_end\ndata: {}\n\n`);
1338
+ res.end();
1374
1339
  } 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 }));
1340
+ // 如果 headers 未发送,返回 JSON 错误;否则关闭连接
1341
+ if (!res.headersSent) {
1342
+ const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'ACCESS_DENIED' ? 403 : 500;
1343
+ res.writeHead(status, { 'Content-Type': 'application/json' });
1344
+ res.end(JSON.stringify({ error: err.message }));
1345
+ } else {
1346
+ res.end();
1347
+ }
1378
1348
  }
1379
1349
  return;
1380
1350
  }
package/lib/translator.js DELETED
@@ -1,84 +0,0 @@
1
- import { readFileSync, existsSync } from 'node:fs';
2
- import { detectLanguage } from '../i18n.js';
3
-
4
- /**
5
- * Determine the target language for translation.
6
- * Priority: explicit `to` param > prefs file lang > system locale.
7
- * @param {string} prefsFile - Path to the preferences JSON file
8
- * @returns {string} target language code
9
- */
10
- export function detectTargetLang(prefsFile) {
11
- let targetLang;
12
- try {
13
- if (prefsFile && existsSync(prefsFile)) {
14
- const prefs = JSON.parse(readFileSync(prefsFile, 'utf-8'));
15
- if (prefs.lang) targetLang = prefs.lang;
16
- }
17
- } catch { }
18
- if (!targetLang) targetLang = detectLanguage();
19
- return targetLang;
20
- }
21
-
22
- /**
23
- * Translate text using the Claude API.
24
- * @param {Object} opts
25
- * @param {string|string[]} opts.text - Text or array of texts to translate
26
- * @param {string} opts.from - Source language code
27
- * @param {string} opts.to - Target language code
28
- * @param {string} opts.apiKey - Anthropic API key
29
- * @param {string} [opts.baseUrl='https://api.anthropic.com'] - API base URL
30
- * @param {string} [opts.model='claude-haiku-4-5-20251001'] - Model to use
31
- * @returns {Promise<{text: string|string[], from: string, to: string}>}
32
- */
33
- export async function translate({ text, from, to, apiKey, baseUrl, model }) {
34
- // Same language — no-op
35
- if (from === to) {
36
- return { text, from, to };
37
- }
38
-
39
- const effectiveBaseUrl = baseUrl || 'https://api.anthropic.com';
40
- const effectiveModel = model || 'claude-haiku-4-5-20251001';
41
- const inputText = Array.isArray(text) ? text.join('\n---SPLIT---\n') : text;
42
-
43
- const reqHeaders = {
44
- 'Content-Type': 'application/json',
45
- 'anthropic-version': '2023-06-01',
46
- 'x-api-key': apiKey,
47
- 'x-cc-viewer-internal': '1',
48
- };
49
-
50
- const apiRes = await fetch(`${effectiveBaseUrl}/v1/messages`, {
51
- method: 'POST',
52
- headers: reqHeaders,
53
- body: JSON.stringify({
54
- model: effectiveModel,
55
- max_tokens: 32000,
56
- tools: [],
57
- system: [{
58
- type: "text",
59
- text: `You are a translator. Translate the following text from ${from} to ${to}. Output only the translated text, nothing else.`
60
- }],
61
- messages: [{ role: 'user', content: inputText }],
62
- stream: false,
63
- temperature: 1,
64
- }),
65
- });
66
-
67
- if (!apiRes.ok) {
68
- const errBody = await apiRes.text();
69
- const err = new Error(`Translation API failed (status ${apiRes.status}): ${errBody}`);
70
- err.status = apiRes.status;
71
- err.detail = errBody;
72
- throw err;
73
- }
74
-
75
- const apiData = await apiRes.json();
76
- let translated = apiData.content?.[0]?.text || '';
77
-
78
- // If input was an array, split the result back into an array
79
- if (Array.isArray(text)) {
80
- translated = translated.split(/\n?---SPLIT---\n?/);
81
- }
82
-
83
- return { text: translated, from, to };
84
- }