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.
- package/dist/assets/{index-D3V7nrsz.js → index-CX2k5Bow.js} +66 -66
- package/dist/assets/{index-Bp_VsiSj.css → index-DGeFNfQD.css} +1 -1
- package/dist/index.html +2 -2
- package/interceptor.js +63 -11
- package/lib/delta-reconstructor.js +169 -0
- package/lib/interceptor-core.js +27 -1
- package/lib/log-management.js +34 -5
- package/lib/log-watcher.js +7 -1
- package/lib/stats-worker.js +5 -2
- package/package.json +1 -1
- package/server.js +31 -8
package/lib/log-management.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
167
|
-
|
|
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]));
|
package/lib/log-watcher.js
CHANGED
|
@@ -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)) {
|
package/lib/stats-worker.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
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
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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 }));
|