cc-viewer 1.4.3 → 1.4.5
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-BDl542y5.css +32 -0
- package/dist/assets/index-Usb6v9iJ.js +617 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/server.js +98 -34
- package/dist/assets/index-59poDh6i.js +0 -609
- package/dist/assets/index-KWmygTKp.css +0 -32
package/dist/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
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-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-Usb6v9iJ.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BDl542y5.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-viewer",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5",
|
|
4
4
|
"description": "Claude Code Logger visualization management tool",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server.js",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"react": "^18.3.1",
|
|
64
64
|
"react-dom": "^18.3.1",
|
|
65
65
|
"react-json-view-lite": "^2.1.0",
|
|
66
|
+
"qrcode.react": "^4.2.0",
|
|
66
67
|
"vite": "^6.3.5"
|
|
67
68
|
},
|
|
68
69
|
"dependencies": {
|
package/server.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
+
import { createConnection } from 'node:net';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
2
4
|
import { readFileSync, writeFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync, renameSync, unlinkSync, openSync, readSync, closeSync } from 'node:fs';
|
|
3
5
|
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import { dirname, join, extname } from 'node:path';
|
|
5
|
-
import { homedir, userInfo, platform } from 'node:os';
|
|
7
|
+
import { homedir, userInfo, platform, networkInterfaces } from 'node:os';
|
|
6
8
|
import { execSync } from 'node:child_process';
|
|
7
9
|
import { Worker } from 'node:worker_threads';
|
|
8
10
|
import { LOG_FILE, _initPromise, _resumeState, resolveResumeChoice, _projectName, _logDir, _cachedApiKey, _cachedAuthHeader, _cachedHaikuModel } from './interceptor.js';
|
|
@@ -46,7 +48,10 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
46
48
|
const __dirname = dirname(__filename);
|
|
47
49
|
const START_PORT = 7008;
|
|
48
50
|
const MAX_PORT = 7099;
|
|
49
|
-
const HOST = '
|
|
51
|
+
const HOST = '0.0.0.0';
|
|
52
|
+
|
|
53
|
+
// 局域网访问 token(本地 127.0.0.1 免验证)
|
|
54
|
+
const ACCESS_TOKEN = randomBytes(16).toString('hex');
|
|
50
55
|
|
|
51
56
|
let clients = [];
|
|
52
57
|
let server;
|
|
@@ -180,7 +185,14 @@ function startWatching() {
|
|
|
180
185
|
}
|
|
181
186
|
|
|
182
187
|
function handleRequest(req, res) {
|
|
183
|
-
const
|
|
188
|
+
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
|
|
189
|
+
const url = parsedUrl.pathname;
|
|
190
|
+
const method = req.method;
|
|
191
|
+
|
|
192
|
+
// WebSocket 路径不处理,交给 upgrade 事件
|
|
193
|
+
if (url === '/ws/terminal') {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
184
196
|
|
|
185
197
|
// CORS headers
|
|
186
198
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -193,6 +205,19 @@ function handleRequest(req, res) {
|
|
|
193
205
|
return;
|
|
194
206
|
}
|
|
195
207
|
|
|
208
|
+
// 局域网访问 token 验证(本地 127.0.0.1 / ::1 免验证,静态资源免验证)
|
|
209
|
+
const remoteIp = req.socket.remoteAddress;
|
|
210
|
+
const isLocal = remoteIp === '127.0.0.1' || remoteIp === '::1' || remoteIp === '::ffff:127.0.0.1';
|
|
211
|
+
const isStaticAsset = url.startsWith('/assets/') || url === '/favicon.ico';
|
|
212
|
+
if (!isLocal && !isStaticAsset) {
|
|
213
|
+
const urlToken = parsedUrl.searchParams.get('token');
|
|
214
|
+
if (urlToken !== ACCESS_TOKEN) {
|
|
215
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
216
|
+
res.end(JSON.stringify({ error: 'Forbidden: invalid token' }));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
196
221
|
// User preferences API
|
|
197
222
|
if (url === '/api/preferences' && method === 'GET') {
|
|
198
223
|
let prefs = {};
|
|
@@ -505,6 +530,24 @@ function handleRequest(req, res) {
|
|
|
505
530
|
return;
|
|
506
531
|
}
|
|
507
532
|
|
|
533
|
+
// 返回局域网访问地址
|
|
534
|
+
if (url === '/api/local-url' && method === 'GET') {
|
|
535
|
+
const nets = networkInterfaces();
|
|
536
|
+
let localIp = '127.0.0.1';
|
|
537
|
+
for (const name of Object.keys(nets)) {
|
|
538
|
+
for (const net of nets[name]) {
|
|
539
|
+
if (net.family === 'IPv4' && !net.internal) {
|
|
540
|
+
localIp = net.address;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (localIp !== '127.0.0.1') break;
|
|
545
|
+
}
|
|
546
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
547
|
+
res.end(JSON.stringify({ url: `http://${localIp}:${actualPort}?token=${ACCESS_TOKEN}` }));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
508
551
|
// 列出本地日志文件(按项目分组,遍历项目子目录)
|
|
509
552
|
if (url === '/api/local-logs' && method === 'GET') {
|
|
510
553
|
try {
|
|
@@ -701,38 +744,48 @@ export async function startViewer() {
|
|
|
701
744
|
return;
|
|
702
745
|
}
|
|
703
746
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const url = `http://${HOST}:${port}`;
|
|
710
|
-
console.error(t('server.started', { host: HOST, port }));
|
|
711
|
-
// v2.0.69 之前的版本会清空控制台,自动打开浏览器确保用户能看到界面
|
|
712
|
-
try {
|
|
713
|
-
const ccPkgPath = join(__dirname, '..', '@anthropic-ai', 'claude-code', 'package.json');
|
|
714
|
-
const ccVer = JSON.parse(readFileSync(ccPkgPath, 'utf-8')).version;
|
|
715
|
-
const [maj, min, pat] = ccVer.split('.').map(Number);
|
|
716
|
-
if (maj < 2 || (maj === 2 && min === 0 && pat < 69)) {
|
|
717
|
-
const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open';
|
|
718
|
-
execSync(`${cmd} ${url}`, { stdio: 'ignore', timeout: 5000 });
|
|
719
|
-
}
|
|
720
|
-
} catch { }
|
|
721
|
-
startWatching();
|
|
722
|
-
startStatsWorker();
|
|
723
|
-
// CLI 模式下启动 WebSocket 服务
|
|
724
|
-
if (isCliMode) {
|
|
725
|
-
setupTerminalWebSocket(currentServer);
|
|
726
|
-
}
|
|
727
|
-
resolve(server);
|
|
747
|
+
// 先检测 127.0.0.1:port 是否已被占用(避免 0.0.0.0 和 127.0.0.1 绑定不冲突的问题)
|
|
748
|
+
const probe = createConnection({ host: '127.0.0.1', port });
|
|
749
|
+
probe.on('connect', () => {
|
|
750
|
+
probe.destroy();
|
|
751
|
+
tryListen(port + 1); // 端口已被占用,尝试下一个
|
|
728
752
|
});
|
|
753
|
+
probe.on('error', () => {
|
|
754
|
+
probe.destroy();
|
|
755
|
+
// 端口空闲,绑定 0.0.0.0
|
|
756
|
+
const currentServer = createServer(handleRequest);
|
|
757
|
+
|
|
758
|
+
currentServer.listen(port, HOST, () => {
|
|
759
|
+
server = currentServer;
|
|
760
|
+
actualPort = port;
|
|
761
|
+
const url = `http://${HOST}:${port}`;
|
|
762
|
+
console.error(t('server.started', { host: HOST, port }));
|
|
763
|
+
// v2.0.69 之前的版本会清空控制台,自动打开浏览器确保用户能看到界面
|
|
764
|
+
try {
|
|
765
|
+
const ccPkgPath = join(__dirname, '..', '@anthropic-ai', 'claude-code', 'package.json');
|
|
766
|
+
const ccVer = JSON.parse(readFileSync(ccPkgPath, 'utf-8')).version;
|
|
767
|
+
const [maj, min, pat] = ccVer.split('.').map(Number);
|
|
768
|
+
if (maj < 2 || (maj === 2 && min === 0 && pat < 69)) {
|
|
769
|
+
const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open';
|
|
770
|
+
execSync(`${cmd} ${url}`, { stdio: 'ignore', timeout: 5000 });
|
|
771
|
+
}
|
|
772
|
+
} catch { }
|
|
773
|
+
startWatching();
|
|
774
|
+
startStatsWorker();
|
|
775
|
+
// CLI 模式下启动 WebSocket 服务
|
|
776
|
+
if (isCliMode) {
|
|
777
|
+
setupTerminalWebSocket(currentServer);
|
|
778
|
+
}
|
|
779
|
+
resolve(server);
|
|
780
|
+
});
|
|
729
781
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
782
|
+
currentServer.on('error', (err) => {
|
|
783
|
+
if (err.code === 'EADDRINUSE') {
|
|
784
|
+
tryListen(port + 1);
|
|
785
|
+
} else {
|
|
786
|
+
reject(err);
|
|
787
|
+
}
|
|
788
|
+
});
|
|
736
789
|
});
|
|
737
790
|
}
|
|
738
791
|
|
|
@@ -745,7 +798,18 @@ async function setupTerminalWebSocket(httpServer) {
|
|
|
745
798
|
const { WebSocketServer } = await import('ws');
|
|
746
799
|
const { writeToPty, resizePty, onPtyData, onPtyExit, getPtyState, getOutputBuffer } = await import('./pty-manager.js');
|
|
747
800
|
|
|
748
|
-
const wss = new WebSocketServer({
|
|
801
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
802
|
+
|
|
803
|
+
httpServer.on('upgrade', (req, socket, head) => {
|
|
804
|
+
const pathname = new URL(req.url, `http://${req.headers.host}`).pathname;
|
|
805
|
+
if (pathname === '/ws/terminal') {
|
|
806
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
807
|
+
wss.emit('connection', ws, req);
|
|
808
|
+
});
|
|
809
|
+
} else {
|
|
810
|
+
socket.destroy();
|
|
811
|
+
}
|
|
812
|
+
});
|
|
749
813
|
|
|
750
814
|
wss.on('connection', (ws) => {
|
|
751
815
|
// 发送当前 PTY 状态
|