cc-viewer 1.6.4 → 1.6.6
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-3VL83o1N.js → index-DS1ml1GF.js} +142 -142
- package/dist/index.html +1 -1
- package/interceptor.js +5 -0
- package/lib/log-watcher.js +5 -1
- package/package.json +1 -1
- package/server.js +25 -14
package/dist/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>Claude Code Viewer</title>
|
|
7
7
|
<link rel="icon" href="/favicon.ico?v=1">
|
|
8
8
|
<link rel="shortcut icon" href="/favicon.ico?v=1">
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DS1ml1GF.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/assets/index-7ty6PCA6.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
package/interceptor.js
CHANGED
|
@@ -440,11 +440,16 @@ export function setupInterceptor() {
|
|
|
440
440
|
delete requestEntry.inProgress;
|
|
441
441
|
delete requestEntry.requestId;
|
|
442
442
|
appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
|
|
443
|
+
// Release memory: clear large objects after disk write
|
|
444
|
+
streamedContent = '';
|
|
445
|
+
requestEntry.response = null;
|
|
443
446
|
} catch (err) {
|
|
444
447
|
requestEntry.response.body = streamedContent.slice(0, 1000);
|
|
445
448
|
delete requestEntry.inProgress;
|
|
446
449
|
delete requestEntry.requestId;
|
|
447
450
|
appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
|
|
451
|
+
streamedContent = '';
|
|
452
|
+
requestEntry.response = null;
|
|
448
453
|
}
|
|
449
454
|
controller.close();
|
|
450
455
|
break;
|
package/lib/log-watcher.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, existsSync, watchFile, openSync, readSync, closeSync, statSync } from 'node:fs';
|
|
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
4
|
|
|
@@ -167,6 +167,10 @@ export function watchLogFile(opts) {
|
|
|
167
167
|
// 检测日志文件是否已轮转到新文件
|
|
168
168
|
const currentLogFile = getLogFile();
|
|
169
169
|
if (currentLogFile !== logFile && !watchedFiles.has(currentLogFile)) {
|
|
170
|
+
// Unwatch old file to prevent watcher leak on rotation
|
|
171
|
+
unwatchFile(logFile);
|
|
172
|
+
watchedFiles.delete(logFile);
|
|
173
|
+
|
|
170
174
|
const newEntries = readLogFile(currentLogFile);
|
|
171
175
|
clients.forEach(client => {
|
|
172
176
|
try {
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -77,7 +77,15 @@ let _workspaceIsNpmVersion = false;
|
|
|
77
77
|
let _workspaceLaunched = false; // 工作区是否已经启动了会话
|
|
78
78
|
|
|
79
79
|
// Editor session state (for $EDITOR intercept)
|
|
80
|
-
const editorSessions = new Map(); // sessionId → { filePath, done }
|
|
80
|
+
const editorSessions = new Map(); // sessionId → { filePath, done, createdAt }
|
|
81
|
+
// Periodically clean up abandoned editor sessions (older than 1 hour)
|
|
82
|
+
const _editorCleanupTimer = setInterval(() => {
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
for (const [id, session] of editorSessions) {
|
|
85
|
+
if (now - (session.createdAt || 0) > 3600000) editorSessions.delete(id);
|
|
86
|
+
}
|
|
87
|
+
}, 60000);
|
|
88
|
+
_editorCleanupTimer.unref(); // Don't keep process alive for cleanup
|
|
81
89
|
let terminalWss = null; // WebSocketServer reference for broadcasting
|
|
82
90
|
export function setWorkspaceClaudeArgs(args) {
|
|
83
91
|
_workspaceClaudeArgs = args;
|
|
@@ -87,6 +95,9 @@ export function setWorkspaceClaudePath(path, isNpm) {
|
|
|
87
95
|
_workspaceIsNpmVersion = isNpm;
|
|
88
96
|
}
|
|
89
97
|
|
|
98
|
+
// Global POST body size limit (10MB) to prevent OOM from malicious/buggy clients
|
|
99
|
+
const MAX_POST_BODY = 10 * 1024 * 1024;
|
|
100
|
+
|
|
90
101
|
|
|
91
102
|
|
|
92
103
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -279,7 +290,7 @@ async function handleRequest(req, res) {
|
|
|
279
290
|
|
|
280
291
|
if (url === '/api/preferences' && method === 'POST') {
|
|
281
292
|
let body = '';
|
|
282
|
-
req.on('data', chunk => { body += chunk; });
|
|
293
|
+
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
283
294
|
req.on('end', () => {
|
|
284
295
|
try {
|
|
285
296
|
const incoming = JSON.parse(body);
|
|
@@ -300,7 +311,7 @@ async function handleRequest(req, res) {
|
|
|
300
311
|
// 注册新的日志文件进行 watch(供新进程复用旧服务时调用)
|
|
301
312
|
if (url === '/api/register-log' && method === 'POST') {
|
|
302
313
|
let body = '';
|
|
303
|
-
req.on('data', chunk => { body += chunk; });
|
|
314
|
+
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
304
315
|
req.on('end', () => {
|
|
305
316
|
try {
|
|
306
317
|
const { logFile } = JSON.parse(body);
|
|
@@ -323,7 +334,7 @@ async function handleRequest(req, res) {
|
|
|
323
334
|
// 用户选择继续/新开日志
|
|
324
335
|
if (url === '/api/resume-choice' && method === 'POST') {
|
|
325
336
|
let body = '';
|
|
326
|
-
req.on('data', chunk => { body += chunk; });
|
|
337
|
+
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
327
338
|
req.on('end', () => {
|
|
328
339
|
try {
|
|
329
340
|
const { choice } = JSON.parse(body);
|
|
@@ -367,7 +378,7 @@ async function handleRequest(req, res) {
|
|
|
367
378
|
// 翻译 API
|
|
368
379
|
if (url === '/api/translate' && method === 'POST') {
|
|
369
380
|
let body = '';
|
|
370
|
-
req.on('data', chunk => { body += chunk; });
|
|
381
|
+
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
371
382
|
req.on('end', async () => {
|
|
372
383
|
try {
|
|
373
384
|
const { text, from = 'en', to } = JSON.parse(body);
|
|
@@ -511,7 +522,7 @@ async function handleRequest(req, res) {
|
|
|
511
522
|
|
|
512
523
|
if (url === '/api/workspaces/launch' && method === 'POST') {
|
|
513
524
|
let body = '';
|
|
514
|
-
req.on('data', chunk => { body += chunk; });
|
|
525
|
+
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
515
526
|
req.on('end', async () => {
|
|
516
527
|
try {
|
|
517
528
|
const { path: wsPath } = JSON.parse(body);
|
|
@@ -570,7 +581,7 @@ async function handleRequest(req, res) {
|
|
|
570
581
|
|
|
571
582
|
if (url === '/api/workspaces/add' && method === 'POST') {
|
|
572
583
|
let body = '';
|
|
573
|
-
req.on('data', chunk => { body += chunk; });
|
|
584
|
+
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
574
585
|
req.on('end', async () => {
|
|
575
586
|
try {
|
|
576
587
|
const { path: wsPath } = JSON.parse(body);
|
|
@@ -902,7 +913,7 @@ async function handleRequest(req, res) {
|
|
|
902
913
|
|
|
903
914
|
if (url === '/api/editor-open' && method === 'POST') {
|
|
904
915
|
let body = '';
|
|
905
|
-
req.on('data', chunk => { body += chunk; });
|
|
916
|
+
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
906
917
|
req.on('end', () => {
|
|
907
918
|
try {
|
|
908
919
|
const { sessionId, filePath } = JSON.parse(body);
|
|
@@ -911,7 +922,7 @@ async function handleRequest(req, res) {
|
|
|
911
922
|
res.end(JSON.stringify({ error: 'Missing sessionId or filePath' }));
|
|
912
923
|
return;
|
|
913
924
|
}
|
|
914
|
-
editorSessions.set(sessionId, { filePath, done: false });
|
|
925
|
+
editorSessions.set(sessionId, { filePath, done: false, createdAt: Date.now() });
|
|
915
926
|
// Broadcast to all terminal WebSocket clients
|
|
916
927
|
if (terminalWss) {
|
|
917
928
|
const msg = JSON.stringify({ type: 'editor-open', sessionId, filePath });
|
|
@@ -946,7 +957,7 @@ async function handleRequest(req, res) {
|
|
|
946
957
|
|
|
947
958
|
if (url === '/api/editor-done' && method === 'POST') {
|
|
948
959
|
let body = '';
|
|
949
|
-
req.on('data', chunk => { body += chunk; });
|
|
960
|
+
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
950
961
|
req.on('end', () => {
|
|
951
962
|
try {
|
|
952
963
|
const { sessionId } = JSON.parse(body);
|
|
@@ -1211,7 +1222,7 @@ async function handleRequest(req, res) {
|
|
|
1211
1222
|
|
|
1212
1223
|
if (url === '/api/plugins/upload' && method === 'POST') {
|
|
1213
1224
|
let body = '';
|
|
1214
|
-
req.on('data', chunk => { body += chunk; });
|
|
1225
|
+
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
1215
1226
|
req.on('end', async () => {
|
|
1216
1227
|
try {
|
|
1217
1228
|
const { files: fileList } = JSON.parse(body);
|
|
@@ -1396,7 +1407,7 @@ async function handleRequest(req, res) {
|
|
|
1396
1407
|
// 删除日志文件
|
|
1397
1408
|
if (url === '/api/delete-logs' && method === 'POST') {
|
|
1398
1409
|
let body = '';
|
|
1399
|
-
req.on('data', chunk => { body += chunk; });
|
|
1410
|
+
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
1400
1411
|
req.on('end', () => {
|
|
1401
1412
|
try {
|
|
1402
1413
|
const { files } = JSON.parse(body);
|
|
@@ -1442,7 +1453,7 @@ async function handleRequest(req, res) {
|
|
|
1442
1453
|
// 合并日志文件
|
|
1443
1454
|
if (url === '/api/merge-logs' && method === 'POST') {
|
|
1444
1455
|
let body = '';
|
|
1445
|
-
req.on('data', chunk => { body += chunk; });
|
|
1456
|
+
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
1446
1457
|
req.on('end', () => {
|
|
1447
1458
|
try {
|
|
1448
1459
|
const { files } = JSON.parse(body);
|
|
@@ -1601,7 +1612,7 @@ async function handleRequest(req, res) {
|
|
|
1601
1612
|
// CCV 进程关闭
|
|
1602
1613
|
if (url === '/api/ccv-processes/kill' && method === 'POST') {
|
|
1603
1614
|
let body = '';
|
|
1604
|
-
req.on('data', chunk => { body += chunk; });
|
|
1615
|
+
req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
|
|
1605
1616
|
req.on('end', async () => {
|
|
1606
1617
|
try {
|
|
1607
1618
|
const { pid } = JSON.parse(body);
|