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/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-59poDh6i.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-KWmygTKp.css">
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",
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 = '127.0.0.1';
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 { url, method } = req;
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
- const currentServer = createServer(handleRequest);
705
-
706
- currentServer.listen(port, HOST, () => {
707
- server = currentServer;
708
- actualPort = port;
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
- currentServer.on('error', (err) => {
731
- if (err.code === 'EADDRINUSE') {
732
- tryListen(port + 1);
733
- } else {
734
- reject(err);
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({ server: httpServer, path: '/ws/terminal' });
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 状态