cc-viewer 0.1.0

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/lib/index.html ADDED
@@ -0,0 +1,57 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Claude Code Viewer</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div class="header">
11
+ <div class="header-left">
12
+ <h1>Claude Code Viewer</h1>
13
+ <div class="status-badge">
14
+ <div class="status-dot"></div>
15
+ <span>实时监控中</span>
16
+ </div>
17
+ </div>
18
+ <div class="header-right">
19
+ <div class="stat-badge">
20
+ 总请求数: <strong id="total-count">0</strong>
21
+ </div>
22
+ <button id="mode-toggle-btn" onclick="toggleViewMode()">打开对话模式</button>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="main-container">
27
+ <div class="left-panel" id="left-panel">
28
+ <div class="panel-header">请求列表</div>
29
+ <div class="request-list" id="request-list">
30
+ <div class="empty-state">
31
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
32
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
33
+ </svg>
34
+ <p>等待请求...</p>
35
+ </div>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="resizer" id="resizer"></div>
40
+
41
+ <div class="right-panel" id="right-panel">
42
+ <div class="empty-detail">
43
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
44
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"></path>
45
+ </svg>
46
+ <p>选择一个请求查看详情</p>
47
+ </div>
48
+ </div>
49
+ </div>
50
+
51
+ <div id="chat-container" class="chat-container" style="display: none;"></div>
52
+
53
+ <script src="/vendor/marked.umd.js"></script>
54
+ <script src="/vendor/json-viewer.bundle.js"></script>
55
+ <script src="/app.js" defer></script>
56
+ </body>
57
+ </html>
package/lib/server.js ADDED
@@ -0,0 +1,222 @@
1
+ import { createServer } from 'node:http';
2
+ import { readFileSync, existsSync, watchFile, unwatchFile } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, join } from 'node:path';
5
+ import { setupInterceptor, LOG_FILE } from '../interceptor.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ const START_PORT = 7008;
10
+ const MAX_PORT = 7099;
11
+ const HOST = '127.0.0.1';
12
+
13
+ let clients = [];
14
+ let server;
15
+ let actualPort = START_PORT;
16
+
17
+ function readLogFile() {
18
+ if (!existsSync(LOG_FILE)) {
19
+ return [];
20
+ }
21
+
22
+ try {
23
+ const content = readFileSync(LOG_FILE, 'utf-8');
24
+ const entries = content.split('\n---\n').filter(line => line.trim());
25
+ return entries.map(entry => {
26
+ try {
27
+ return JSON.parse(entry);
28
+ } catch {
29
+ return null;
30
+ }
31
+ }).filter(Boolean);
32
+ } catch (err) {
33
+ console.error('Error reading log file:', err);
34
+ return [];
35
+ }
36
+ }
37
+
38
+ function sendToClients(entry) {
39
+ clients.forEach(client => {
40
+ try {
41
+ client.write(`data: ${JSON.stringify(entry)}\n\n`);
42
+ } catch (err) {
43
+ // Client disconnected
44
+ }
45
+ });
46
+ }
47
+
48
+ function startWatching() {
49
+ let lastSize = 0;
50
+ watchFile(LOG_FILE, { interval: 500 }, () => {
51
+ try {
52
+ const content = readFileSync(LOG_FILE, 'utf-8');
53
+ const newContent = content.slice(lastSize);
54
+ lastSize = content.length;
55
+
56
+ if (newContent.trim()) {
57
+ const entries = newContent.split('\n---\n').filter(line => line.trim());
58
+ entries.forEach(entry => {
59
+ try {
60
+ const parsed = JSON.parse(entry);
61
+ sendToClients(parsed);
62
+ } catch (err) {
63
+ // Skip invalid entries
64
+ }
65
+ });
66
+ }
67
+ } catch (err) {
68
+ // File not yet created, will retry on next poll
69
+ }
70
+ });
71
+ }
72
+
73
+ function handleRequest(req, res) {
74
+ const { url, method } = req;
75
+
76
+ // CORS headers
77
+ res.setHeader('Access-Control-Allow-Origin', '*');
78
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
79
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
80
+
81
+ if (method === 'OPTIONS') {
82
+ res.writeHead(200);
83
+ res.end();
84
+ return;
85
+ }
86
+
87
+ // Serve HTML
88
+ if (url === '/' && method === 'GET') {
89
+ try {
90
+ const htmlPath = join(__dirname, 'index.html');
91
+ const html = readFileSync(htmlPath, 'utf-8');
92
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
93
+ res.end(html);
94
+ } catch (err) {
95
+ res.writeHead(500);
96
+ res.end('Error loading page');
97
+ }
98
+ }
99
+ // Serve CSS
100
+ else if (url === '/style.css' && method === 'GET') {
101
+ try {
102
+ const cssPath = join(__dirname, 'style.css');
103
+ const css = readFileSync(cssPath, 'utf-8');
104
+ res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8' });
105
+ res.end(css);
106
+ } catch (err) {
107
+ res.writeHead(500);
108
+ res.end('Error loading CSS');
109
+ }
110
+ }
111
+ // Serve JavaScript
112
+ else if (url === '/app.js' && method === 'GET') {
113
+ try {
114
+ const jsPath = join(__dirname, 'app.js');
115
+ const js = readFileSync(jsPath, 'utf-8');
116
+ res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
117
+ res.end(js);
118
+ } catch (err) {
119
+ res.writeHead(500);
120
+ res.end('Error loading JavaScript');
121
+ }
122
+ }
123
+ // Serve node_modules files
124
+ else if (url.startsWith('/vendor/') && method === 'GET') {
125
+ try {
126
+ const filePath = join(__dirname, url);
127
+ const content = readFileSync(filePath);
128
+
129
+ let contentType = 'application/octet-stream';
130
+ if (url.endsWith('.js')) {
131
+ contentType = 'application/javascript; charset=utf-8';
132
+ } else if (url.endsWith('.css')) {
133
+ contentType = 'text/css; charset=utf-8';
134
+ }
135
+
136
+ res.writeHead(200, { 'Content-Type': contentType });
137
+ res.end(content);
138
+ } catch (err) {
139
+ res.writeHead(404);
140
+ res.end('File not found');
141
+ }
142
+ }
143
+ // SSE endpoint
144
+ else if (url === '/events' && method === 'GET') {
145
+ res.writeHead(200, {
146
+ 'Content-Type': 'text/event-stream',
147
+ 'Cache-Control': 'no-cache',
148
+ 'Connection': 'keep-alive',
149
+ });
150
+
151
+ clients.push(res);
152
+
153
+ const entries = readLogFile();
154
+ entries.forEach(entry => {
155
+ res.write(`data: ${JSON.stringify(entry)}\n\n`);
156
+ });
157
+
158
+ req.on('close', () => {
159
+ clients = clients.filter(client => client !== res);
160
+ });
161
+ }
162
+ // API endpoint
163
+ else if (url === '/api/requests' && method === 'GET') {
164
+ const entries = readLogFile();
165
+ res.writeHead(200, { 'Content-Type': 'application/json' });
166
+ res.end(JSON.stringify(entries));
167
+ }
168
+ else {
169
+ res.writeHead(404);
170
+ res.end('Not Found');
171
+ }
172
+ }
173
+
174
+ export function startViewer() {
175
+ // 首先设置拦截器
176
+ setupInterceptor();
177
+
178
+ return new Promise((resolve, reject) => {
179
+ function tryListen(port) {
180
+ if (port > MAX_PORT) {
181
+ console.log(`⚠️ 端口 ${START_PORT}-${MAX_PORT} 均被占用,请求监控服务未启动`);
182
+ resolve(null);
183
+ return;
184
+ }
185
+
186
+ const currentServer = createServer(handleRequest);
187
+
188
+ currentServer.listen(port, HOST, () => {
189
+ server = currentServer;
190
+ actualPort = port;
191
+ console.log(`\n🔍 Claude 请求监控服务已启动: http://${HOST}:${port}\n`);
192
+ startWatching();
193
+ resolve(server);
194
+ });
195
+
196
+ currentServer.on('error', (err) => {
197
+ if (err.code === 'EADDRINUSE') {
198
+ console.log(`⚠️ 端口 ${port} 已被占用,尝试 ${port + 1}...`);
199
+ tryListen(port + 1);
200
+ } else {
201
+ reject(err);
202
+ }
203
+ });
204
+ }
205
+
206
+ tryListen(START_PORT);
207
+ });
208
+ }
209
+
210
+ export function stopViewer() {
211
+ unwatchFile(LOG_FILE);
212
+ clients.forEach(client => client.end());
213
+ clients = [];
214
+ if (server) {
215
+ server.close();
216
+ }
217
+ }
218
+
219
+ // Auto-start the viewer when imported
220
+ startViewer().catch(err => {
221
+ console.error('Failed to start CC Viewer:', err);
222
+ });