cc-viewer 1.0.2 → 1.0.4
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/README.md +3 -7
- package/cli.js +1 -1
- package/{lib/assets/index-Cc1O-iKV.js → dist/assets/index-DZBQn9fO.js} +125 -125
- package/{lib → dist}/index.html +1 -1
- package/locales/ar.json +2 -3
- package/locales/da.json +2 -3
- package/locales/de.json +2 -3
- package/locales/en.json +2 -3
- package/locales/es.json +2 -3
- package/locales/fr.json +2 -3
- package/locales/it.json +2 -3
- package/locales/ja.json +2 -3
- package/locales/ko.json +2 -3
- package/locales/no.json +2 -3
- package/locales/pl.json +2 -3
- package/locales/pt-BR.json +2 -3
- package/locales/ru.json +2 -3
- package/locales/th.json +2 -3
- package/locales/tr.json +2 -3
- package/locales/uk.json +2 -3
- package/locales/zh-TW.json +2 -3
- package/locales/zh.json +2 -3
- package/package.json +7 -6
- package/{lib/server.js → server.js} +66 -122
- /package/{lib → dist}/assets/index-C7-c9XfU.css +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { createServer
|
|
2
|
-
import { readFileSync, existsSync, watchFile, unwatchFile, statSync,
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { readFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { dirname, join, extname, basename } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
|
-
import { LOG_FILE } from '
|
|
7
|
-
import { t } from '
|
|
6
|
+
import { LOG_FILE } from './interceptor.js';
|
|
7
|
+
import { t } from './i18n.js';
|
|
8
8
|
|
|
9
9
|
const LOG_DIR = join(homedir(), '.claude', 'cc-viewer');
|
|
10
10
|
const SHOW_ALL_FILE = '/tmp/cc-viewer-show-all';
|
|
@@ -14,51 +14,12 @@ const __dirname = dirname(__filename);
|
|
|
14
14
|
const START_PORT = 7008;
|
|
15
15
|
const MAX_PORT = 7099;
|
|
16
16
|
const HOST = '127.0.0.1';
|
|
17
|
-
const PORT_FILE = '/tmp/cc-viewer-port';
|
|
18
|
-
const LOCK_FILE = '/tmp/cc-viewer-lock';
|
|
19
|
-
|
|
20
|
-
function acquireLock() {
|
|
21
|
-
try {
|
|
22
|
-
// wx flag: exclusive create, fails if file already exists
|
|
23
|
-
const fd = openSync(LOCK_FILE, 'wx');
|
|
24
|
-
writeFileSync(fd, String(process.pid));
|
|
25
|
-
closeSync(fd);
|
|
26
|
-
return true;
|
|
27
|
-
} catch {
|
|
28
|
-
// 检查锁文件是否过期(超过 10 秒视为过期)
|
|
29
|
-
try {
|
|
30
|
-
const stat = statSync(LOCK_FILE);
|
|
31
|
-
if (Date.now() - stat.mtimeMs > 10000) {
|
|
32
|
-
unlinkSync(LOCK_FILE);
|
|
33
|
-
const fd = openSync(LOCK_FILE, 'wx');
|
|
34
|
-
writeFileSync(fd, String(process.pid));
|
|
35
|
-
closeSync(fd);
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
} catch {}
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function releaseLock() {
|
|
44
|
-
try { unlinkSync(LOCK_FILE); } catch {}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function checkPortAlive(port) {
|
|
48
|
-
return new Promise((resolve) => {
|
|
49
|
-
const req = httpRequest({ host: HOST, port, path: '/api/requests', method: 'GET', timeout: 1000 }, (res) => {
|
|
50
|
-
res.resume();
|
|
51
|
-
resolve(true);
|
|
52
|
-
});
|
|
53
|
-
req.on('error', () => resolve(false));
|
|
54
|
-
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
55
|
-
req.end();
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
17
|
|
|
59
18
|
let clients = [];
|
|
60
19
|
let server;
|
|
61
20
|
let actualPort = START_PORT;
|
|
21
|
+
// 跟踪所有被 watch 的日志文件
|
|
22
|
+
const watchedFiles = new Map();
|
|
62
23
|
|
|
63
24
|
const MIME_TYPES = {
|
|
64
25
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -103,11 +64,13 @@ function sendToClients(entry) {
|
|
|
103
64
|
});
|
|
104
65
|
}
|
|
105
66
|
|
|
106
|
-
function
|
|
67
|
+
function watchLogFile(logFile) {
|
|
68
|
+
if (watchedFiles.has(logFile)) return;
|
|
107
69
|
let lastSize = 0;
|
|
108
|
-
|
|
70
|
+
watchedFiles.set(logFile, true);
|
|
71
|
+
watchFile(logFile, { interval: 500 }, () => {
|
|
109
72
|
try {
|
|
110
|
-
const content = readFileSync(
|
|
73
|
+
const content = readFileSync(logFile, 'utf-8');
|
|
111
74
|
const newContent = content.slice(lastSize);
|
|
112
75
|
lastSize = content.length;
|
|
113
76
|
|
|
@@ -128,6 +91,10 @@ function startWatching() {
|
|
|
128
91
|
});
|
|
129
92
|
}
|
|
130
93
|
|
|
94
|
+
function startWatching() {
|
|
95
|
+
watchLogFile(LOG_FILE);
|
|
96
|
+
}
|
|
97
|
+
|
|
131
98
|
function handleRequest(req, res) {
|
|
132
99
|
const { url, method } = req;
|
|
133
100
|
|
|
@@ -142,6 +109,29 @@ function handleRequest(req, res) {
|
|
|
142
109
|
return;
|
|
143
110
|
}
|
|
144
111
|
|
|
112
|
+
// 注册新的日志文件进行 watch(供新进程复用旧服务时调用)
|
|
113
|
+
if (url === '/api/register-log' && method === 'POST') {
|
|
114
|
+
let body = '';
|
|
115
|
+
req.on('data', chunk => { body += chunk; });
|
|
116
|
+
req.on('end', () => {
|
|
117
|
+
try {
|
|
118
|
+
const { logFile } = JSON.parse(body);
|
|
119
|
+
if (logFile && typeof logFile === 'string' && logFile.startsWith(LOG_DIR) && existsSync(logFile)) {
|
|
120
|
+
watchLogFile(logFile);
|
|
121
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
122
|
+
res.end(JSON.stringify({ ok: true }));
|
|
123
|
+
} else {
|
|
124
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
125
|
+
res.end(JSON.stringify({ error: 'Invalid log file path' }));
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
129
|
+
res.end(JSON.stringify({ error: 'Invalid request body' }));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
145
135
|
// SSE endpoint
|
|
146
136
|
if (url === '/events' && method === 'GET') {
|
|
147
137
|
res.writeHead(200, {
|
|
@@ -297,88 +287,42 @@ function handleRequest(req, res) {
|
|
|
297
287
|
}
|
|
298
288
|
|
|
299
289
|
export async function startViewer() {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const existingPort = parseInt(readFileSync(PORT_FILE, 'utf-8').trim(), 10);
|
|
307
|
-
if (existingPort >= START_PORT && existingPort <= MAX_PORT) {
|
|
308
|
-
const alive = await checkPortAlive(existingPort);
|
|
309
|
-
if (alive) {
|
|
310
|
-
actualPort = existingPort;
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
} catch {}
|
|
315
|
-
}
|
|
316
|
-
// 等待后仍无法复用,静默退出
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
try {
|
|
321
|
-
// 检查是否已有 cc-viewer 实例在运行
|
|
322
|
-
if (existsSync(PORT_FILE)) {
|
|
323
|
-
try {
|
|
324
|
-
const existingPort = parseInt(readFileSync(PORT_FILE, 'utf-8').trim(), 10);
|
|
325
|
-
if (existingPort >= START_PORT && existingPort <= MAX_PORT) {
|
|
326
|
-
const alive = await checkPortAlive(existingPort);
|
|
327
|
-
if (alive) {
|
|
328
|
-
actualPort = existingPort;
|
|
329
|
-
releaseLock();
|
|
330
|
-
console.log(t('server.reuse', { host: HOST, port: existingPort }));
|
|
331
|
-
return null;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
} catch {
|
|
335
|
-
// PORT_FILE 读取失败,继续正常启动
|
|
290
|
+
return new Promise((resolve, reject) => {
|
|
291
|
+
function tryListen(port) {
|
|
292
|
+
if (port > MAX_PORT) {
|
|
293
|
+
console.log(t('server.portsBusy', { start: START_PORT, end: MAX_PORT }));
|
|
294
|
+
resolve(null);
|
|
295
|
+
return;
|
|
336
296
|
}
|
|
337
|
-
// 旧实例已不存在,清理 PORT_FILE
|
|
338
|
-
try { unlinkSync(PORT_FILE); } catch {}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return new Promise((resolve, reject) => {
|
|
342
|
-
function tryListen(port) {
|
|
343
|
-
if (port > MAX_PORT) {
|
|
344
|
-
console.log(t('server.portsBusy', { start: START_PORT, end: MAX_PORT }));
|
|
345
|
-
releaseLock();
|
|
346
|
-
resolve(null);
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
297
|
|
|
350
|
-
|
|
298
|
+
const currentServer = createServer(handleRequest);
|
|
351
299
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
resolve(server);
|
|
360
|
-
});
|
|
300
|
+
currentServer.listen(port, HOST, () => {
|
|
301
|
+
server = currentServer;
|
|
302
|
+
actualPort = port;
|
|
303
|
+
console.log(t('server.started', { host: HOST, port }));
|
|
304
|
+
startWatching();
|
|
305
|
+
resolve(server);
|
|
306
|
+
});
|
|
361
307
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
308
|
+
currentServer.on('error', (err) => {
|
|
309
|
+
if (err.code === 'EADDRINUSE') {
|
|
310
|
+
tryListen(port + 1);
|
|
311
|
+
} else {
|
|
312
|
+
reject(err);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
371
316
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
} catch (err) {
|
|
375
|
-
releaseLock();
|
|
376
|
-
throw err;
|
|
377
|
-
}
|
|
317
|
+
tryListen(START_PORT);
|
|
318
|
+
});
|
|
378
319
|
}
|
|
379
320
|
|
|
380
321
|
export function stopViewer() {
|
|
381
|
-
|
|
322
|
+
for (const logFile of watchedFiles.keys()) {
|
|
323
|
+
unwatchFile(logFile);
|
|
324
|
+
}
|
|
325
|
+
watchedFiles.clear();
|
|
382
326
|
clients.forEach(client => client.end());
|
|
383
327
|
clients = [];
|
|
384
328
|
if (server) {
|
|
File without changes
|