cc-viewer 1.6.26 → 1.6.27

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.
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, writeFileSync, existsSync, statSync, readdirSync, unlinkSync, realpathSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { reconstructEntries } from './delta-reconstructor.js';
3
4
 
4
5
  /**
5
6
  * Validate that a resolved file path is contained within logDir.
@@ -77,10 +78,16 @@ export function readLocalLog(logDir, file) {
77
78
  validateLogPath(logDir, file);
78
79
  const filePath = join(logDir, file);
79
80
  const content = readFileSync(filePath, 'utf-8');
80
- const entries = content.split('\n---\n').filter(line => line.trim()).map(entry => {
81
+ const parsed = content.split('\n---\n').filter(line => line.trim()).map(entry => {
81
82
  try { return JSON.parse(entry); } catch { return null; }
82
83
  }).filter(Boolean);
83
- return entries;
84
+ // Delta storage: 先去重(timestamp|url),再重建 delta 条目
85
+ const map = new Map();
86
+ for (const entry of parsed) {
87
+ const key = `${entry.timestamp}|${entry.url}`;
88
+ map.set(key, entry);
89
+ }
90
+ return reconstructEntries(Array.from(map.values()));
84
91
  }
85
92
 
86
93
  /**
@@ -160,11 +167,33 @@ export function mergeLogFiles(logDir, files) {
160
167
  err.code = 'INVALID_INPUT';
161
168
  throw err;
162
169
  }
163
- // 合并内容写入第一个文件
170
+ // Delta storage: 合并时先重建为全量格式再拼接(输出全部为旧格式全量条目,无 _deltaFormat)
164
171
  const targetFile = files[0];
165
172
  const targetPath = join(logDir, targetFile);
166
- const contents = files.map(f => readFileSync(join(logDir, f), 'utf-8').trimEnd());
167
- writeFileSync(targetPath, contents.join('\n---\n') + '\n');
173
+ const allEntries = [];
174
+ for (const f of files) {
175
+ const content = readFileSync(join(logDir, f), 'utf-8');
176
+ const parsed = content.split('\n---\n').filter(line => line.trim()).map(entry => {
177
+ try { return JSON.parse(entry); } catch { return null; }
178
+ }).filter(Boolean);
179
+ // 去重
180
+ const map = new Map();
181
+ for (const entry of parsed) {
182
+ const key = `${entry.timestamp}|${entry.url}`;
183
+ map.set(key, entry);
184
+ }
185
+ // 重建 delta 为全量
186
+ const reconstructed = reconstructEntries(Array.from(map.values()));
187
+ // 清除 delta 元字段,输出为旧格式全量条目
188
+ for (const entry of reconstructed) {
189
+ delete entry._deltaFormat;
190
+ delete entry._totalMessageCount;
191
+ delete entry._conversationId;
192
+ delete entry._isCheckpoint;
193
+ }
194
+ allEntries.push(...reconstructed);
195
+ }
196
+ writeFileSync(targetPath, allEntries.map(e => JSON.stringify(e)).join('\n---\n') + '\n---\n');
168
197
  // 删除其余文件
169
198
  for (let i = 1; i < files.length; i++) {
170
199
  unlinkSync(join(logDir, files[i]));
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, existsSync, watchFile, unwatchFile, openSync, readSync, closeSync, statSync } from 'node:fs';
2
2
  import { isMainAgentEntry, extractCachedContent } from './kv-cache-analyzer.js';
3
3
  import { buildContextWindowEvent, getContextSizeForModel } from './context-watcher.js';
4
+ import { reconstructEntries, createIncrementalReconstructor } from './delta-reconstructor.js';
4
5
 
5
6
  // 跟踪所有被 watch 的日志文件
6
7
  const watchedFiles = new Map();
@@ -31,7 +32,7 @@ export function readLogFile(logFile) {
31
32
  const key = `${entry.timestamp}|${entry.url}`;
32
33
  map.set(key, entry);
33
34
  }
34
- return Array.from(map.values());
35
+ return reconstructEntries(Array.from(map.values()));
35
36
  } catch (err) {
36
37
  console.error('Error reading log file:', err);
37
38
  return [];
@@ -84,6 +85,8 @@ export function watchLogFile(opts) {
84
85
  // Track byte offset instead of string length — avoids full-file re-read on every poll
85
86
  let lastByteOffset = 0;
86
87
  let pendingTail = ''; // incomplete entry carried across polls
88
+ // Delta storage: 增量重建器,用于逐条重建 mainAgent delta 条目
89
+ const _reconstructor = createIncrementalReconstructor();
87
90
  try {
88
91
  if (existsSync(logFile)) {
89
92
  lastByteOffset = statSync(logFile).size;
@@ -99,6 +102,7 @@ export function watchLogFile(opts) {
99
102
  if (currentSize < lastByteOffset) {
100
103
  lastByteOffset = 0;
101
104
  pendingTail = '';
105
+ _reconstructor.reset();
102
106
 
103
107
  // 文件被清空可能是轮转信号,立即检查是否已切换到新文件
104
108
  const currentLogFile = getLogFile();
@@ -157,6 +161,8 @@ export function watchLogFile(opts) {
157
161
  if (!parsed.pid) {
158
162
  parsed.pid = getClaudePid();
159
163
  }
164
+ // Delta storage: reconstruct before push — 确保前端收到完整 messages
165
+ _reconstructor.reconstruct(parsed);
160
166
  sendToClients(clients, parsed);
161
167
  runParallelHook('onNewEntry', parsed).catch(() => {});
162
168
  if (isMainAgentEntry(parsed)) {
@@ -108,7 +108,8 @@ function parseJsonlFile(filePath) {
108
108
  // 会话轮次统计(仅 MainAgent,排除 SUGGESTION MODE)
109
109
  if (entry.mainAgent && Array.isArray(entry.body?.messages)) {
110
110
  const msgs = entry.body.messages;
111
- const len = msgs.length;
111
+ // Delta storage: 使用 _totalMessageCount(delta 条目)或 msgs.length(旧格式)
112
+ const len = entry._totalMessageCount || msgs.length;
112
113
  if (!isSuggestionMode(msgs)) {
113
114
  // messages 数量大幅缩减:新会话(/clear 等),重置追踪
114
115
  if (len < maxMsgLen * 0.5 && (maxMsgLen - len) > 4) {
@@ -129,12 +130,14 @@ function parseJsonlFile(filePath) {
129
130
  maxMsgLen = len;
130
131
  }
131
132
  const texts = extractUserTexts(msgs);
133
+ // Delta storage: delta 条目的 msgs 只有新增部分,prevTextCount 不适用,从 0 开始收集
134
+ const textStart = entry._deltaFormat ? 0 : prevTextCount;
132
135
  // 生成本次 prompt 签名,用于跳过同轮重复请求
133
136
  const sig = texts.join('\x00');
134
137
  if (sig === lastCollectedSig && !isNewTurn) {
135
138
  // 同一轮的重复请求(内容完全相同),跳过
136
139
  } else {
137
- for (let ti = prevTextCount; ti < texts.length; ti++) {
140
+ for (let ti = textStart; ti < texts.length; ti++) {
138
141
  const flat = texts[ti].replace(/[\r\n]+/g, ' ').trim();
139
142
  if (flat) preview.push(flat.slice(0, 100));
140
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.6.26",
3
+ "version": "1.6.27",
4
4
  "description": "Claude Code Logger visualization management tool",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -1313,14 +1313,37 @@ async function handleRequest(req, res) {
1313
1313
  return;
1314
1314
  }
1315
1315
  const fileName = file.split('/').pop();
1316
- const stat = statSync(realPath);
1317
- res.writeHead(200, {
1318
- 'Content-Type': 'application/octet-stream',
1319
- 'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
1320
- 'Content-Length': stat.size,
1321
- });
1322
- const stream = createReadStream(realPath);
1323
- stream.pipe(res);
1316
+ const format = parsedUrl.searchParams.get('format');
1317
+ // Delta storage: format=raw 下载原始文件;默认下载重建后的全量格式
1318
+ if (format === 'raw') {
1319
+ const stat = statSync(realPath);
1320
+ res.writeHead(200, {
1321
+ 'Content-Type': 'application/octet-stream',
1322
+ 'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
1323
+ 'Content-Length': stat.size,
1324
+ });
1325
+ const stream = createReadStream(realPath);
1326
+ stream.pipe(res);
1327
+ } 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');
1340
+ res.writeHead(200, {
1341
+ 'Content-Type': 'application/octet-stream',
1342
+ 'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
1343
+ 'Content-Length': buf.length,
1344
+ });
1345
+ res.end(buf);
1346
+ }
1324
1347
  } catch (err) {
1325
1348
  res.writeHead(500, { 'Content-Type': 'application/json' });
1326
1349
  res.end(JSON.stringify({ error: err.message }));