claude-code-inspector 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.
@@ -0,0 +1,31 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ jobs:
10
+ build-and-test:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Setup Node.js
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: '20'
20
+ cache: 'yarn'
21
+
22
+ - name: Install dependencies
23
+ run: yarn install --frozen-lockfile
24
+ env:
25
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
26
+
27
+ - name: Build
28
+ run: yarn build
29
+
30
+ - name: Run tests
31
+ run: yarn test
@@ -0,0 +1,33 @@
1
+ name: Publish npm Package
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Setup Node.js
15
+ uses: actions/setup-node@v4
16
+ with:
17
+ node-version: '20'
18
+ cache: 'yarn'
19
+ registry-url: 'https://registry.npmjs.org'
20
+
21
+ - name: Install dependencies
22
+ run: yarn install --frozen-lockfile
23
+
24
+ - name: Build
25
+ run: yarn build
26
+
27
+ - name: Run tests
28
+ run: yarn test
29
+
30
+ - name: Publish to npm
31
+ run: npm publish --access public
32
+ env:
33
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # CC Inspector
2
+
3
+ CC Inspector 是一个用于监控和记录 Claude Code API 请求的开发者工具。它通过代理拦截 `/v1/messages` 请求,记录详细的请求/响应数据,并提供实时可视化面板。
4
+
5
+ ## 功能特性
6
+
7
+ - **请求代理**: 拦截并转发 Claude Code API 请求到上游服务器
8
+ - **实时日志**: 记录所有请求的 headers、body、response 和 streaming events
9
+ - **监控面板**: 可视化展示请求状态、tokens 使用量、延迟和成本
10
+ - **WebSocket 推送**: 实时推送新请求和更新到前端
11
+ - **数据持久化**: 使用 SQLite 存储所有请求日志
12
+ - **导出功能**: 支持 JSON 和 CSV 格式导出请求数据
13
+ - **Token 统计**: 自动计算 input/output tokens 和缓存使用量
14
+ - **成本估算**: 根据模型自动计算每次请求的成本
15
+
16
+ ## 技术栈
17
+
18
+ - **框架**: Next.js 16 + React 19
19
+ - **语言**: TypeScript
20
+ - **数据库**: SQLite (better-sqlite3)
21
+ - **WebSocket**: ws
22
+ - **样式**: Tailwind CSS 4
23
+ - **测试**: Vitest
24
+
25
+ ## 快速开始
26
+
27
+ ### 环境要求
28
+
29
+ - Node.js 18+
30
+ - npm / yarn / pnpm
31
+
32
+ ### 安装依赖
33
+
34
+ ```bash
35
+ npm install
36
+ ```
37
+
38
+ ### 启动开发服务器
39
+
40
+ ```bash
41
+ npm run dev
42
+ ```
43
+
44
+ 访问以下地址:
45
+ - **Dashboard**: http://localhost:3000/dashboard
46
+ - **首页**: http://localhost:3000
47
+
48
+ ### 配置 LLM 服务 API
49
+
50
+ CC Inspector 需要知道将请求转发到哪个 LLM 服务提供商。配置方式有两种:
51
+
52
+ **方式 1:在项目根目录创建 `.env.local` 文件**
53
+
54
+ ```bash
55
+ # .env.local
56
+ UPSTREAM_BASE_URL=https://api.anthropic.com # 上游 API 基础 URL
57
+ UPSTREAM_API_KEY=your-api-key # 上游 API Key
58
+ ```
59
+
60
+ **方式 2:在 Claude Code 全局配置中设置 (`~/.claude/settings.json`)**
61
+
62
+ ```json
63
+ {
64
+ "env": {
65
+ "UPSTREAM_BASE_URL": "https://api.anthropic.com",
66
+ "UPSTREAM_API_KEY": "your-api-key"
67
+ }
68
+ }
69
+ ```
70
+
71
+ > 注意:如果未设置 `UPSTREAM_BASE_URL`,程序会自动使用 `ANTHROPIC_BASE_URL` 的值。
72
+
73
+ ### 配置 Claude Code 使用代理
74
+
75
+ CC Inspector 启动后,需要让 Claude Code 将请求发送到代理服务器而不是直接发送到 Anthropic API。
76
+
77
+ 在 Claude Code 中执行以下命令配置 baseURL:
78
+
79
+ ```bash
80
+ /mcp set anthropic_base_url http://localhost:3000/api/proxy
81
+ ```
82
+
83
+ 或者手动编辑 `~/.claude/settings.json`:
84
+
85
+ ```json
86
+ {
87
+ "anthropic_base_url": "http://localhost:3000/api/proxy"
88
+ }
89
+ ```
90
+
91
+ 配置完成后,Claude Code 的所有 API 请求都会先经过 CC Inspector,然后转发到上游 API。
92
+
93
+ **验证配置:**
94
+
95
+ 1. 启动 CC Inspector:`npm run dev`
96
+ 2. 访问 Dashboard:http://localhost:3000/dashboard
97
+ 3. 在 Claude Code 中发起任意请求
98
+ 4. Dashboard 应显示新请求的记录
99
+
100
+ ## 项目结构
101
+
102
+ ```
103
+ cc-inspector/
104
+ ├── app/ # Next.js App Router
105
+ │ ├── dashboard/ # 监控面板页面
106
+ │ ├── api/ # API 路由
107
+ │ │ ├── proxy/ # 代理端点
108
+ │ │ ├── requests/ # 请求日志 API
109
+ │ │ └── events/ # SSE 事件 API
110
+ │ └── v1/messages/ # 原始消息端点
111
+ ├── lib/ # 核心逻辑库
112
+ │ ├── proxy/ # 代理转发器
113
+ │ │ ├── handlers.ts # 请求处理器
114
+ │ │ ├── forwarder.ts # 转发器
115
+ │ │ └── ws-server.ts # WebSocket 服务器
116
+ │ └── recorder/ # 数据记录器
117
+ │ ├── index.ts # 记录器入口
118
+ │ ├── store.ts # SQLite 存储
119
+ │ └── schema.ts # 数据库 Schema
120
+ ├── components/ # React 组件
121
+ │ ├── JsonViewer.tsx # JSON 查看器
122
+ │ └── JsonModal.tsx # JSON 模态框
123
+ ├── db/ # SQLite 数据库文件
124
+ └── server.ts # 自定义服务器(WebSocket + Next.js)
125
+ ```
126
+
127
+ ## API 端点
128
+
129
+ | 端点 | 方法 | 描述 |
130
+ |------|------|------|
131
+ | `/api/proxy` | POST | 代理转发请求到上游 |
132
+ | `/api/requests` | GET | 获取最近的请求日志 |
133
+ | `/api/requests/:id` | GET | 获取单个请求详情 |
134
+ | `/api/requests/export` | GET | 导出请求数据(JSON/CSV) |
135
+ | `/api/events` | GET | SSE 事件流 |
136
+ | `/api/ws` | WebSocket | 实时推送连接 |
137
+
138
+ ## 数据模型
139
+
140
+ 请求日志包含以下字段:
141
+
142
+ - `id`: 请求唯一标识
143
+ - `session_id`: 会话标识
144
+ - `endpoint`: 请求端点
145
+ - `method`: HTTP 方法
146
+ - `request_headers/body`: 请求头和请求体
147
+ - `response_status/body/headers`: 响应状态、响应体和响应头
148
+ - `streaming_events`: SSE 流式事件
149
+ - `input_tokens/output_tokens`: 输入/输出 tokens
150
+ - `cache_read_tokens/cache_creation_tokens`: 缓存读取/创建 tokens
151
+ - `latency_ms`: 请求延迟(毫秒)
152
+ - `first_token_ms`: 首 token 时间(毫秒)
153
+ - `model`: 使用的模型
154
+ - `cost_usd`: 估算成本(美元)
155
+ - `error_message`: 错误信息(如有)
156
+
157
+ ## 脚本命令
158
+
159
+ ```bash
160
+ # 开发
161
+ npm run dev # 启动开发服务器
162
+
163
+ # 构建和运行
164
+ npm run build # 构建生产版本
165
+ npm run start # 启动生产服务器
166
+
167
+ # 测试和检查
168
+ npm run test # 运行测试
169
+ npm run lint # ESLint 检查
170
+ ```
171
+
172
+ ## 数据库
173
+
174
+ 数据存储在 `db/inspector.sqlite`,包含以下表:
175
+
176
+ - `request_logs`: 请求日志
177
+ - `settings`: 配置设置
178
+
179
+ ## 注意事项
180
+
181
+ 1. **WebSocket**: 生产环境建议使用 `wss://` 连接
182
+ 2. **代理模式**: 确保 Claude Code 配置为使用代理端点
183
+ 3. **Token 计算**: 成本估算基于官方定价,仅供参考
184
+
185
+ ## 开发
186
+
187
+ ### 运行测试
188
+
189
+ ```bash
190
+ npm run test
191
+ ```
192
+
193
+ ### 调试
194
+
195
+ 服务器日志会输出所有请求和 WebSocket 连接信息。查看控制台输出或使用 `db/inspector.sqlite` 直接查询数据库。
196
+
197
+ ## License
198
+
199
+ MIT
@@ -0,0 +1,35 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ /**
4
+ * SSE 实时推送端点
5
+ */
6
+ export async function GET(request: NextRequest) {
7
+ const encoder = new TextEncoder();
8
+
9
+ const stream = new ReadableStream({
10
+ async start(controller) {
11
+ // 发送初始连接消息
12
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'connected', timestamp: Date.now() })}\n\n`));
13
+
14
+ // 定期发送心跳
15
+ const heartbeat = setInterval(() => {
16
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'heartbeat', timestamp: Date.now() })}\n\n`));
17
+ }, 30000);
18
+
19
+ // 当客户端断开时清理
20
+ request.signal.addEventListener('abort', () => {
21
+ clearInterval(heartbeat);
22
+ controller.close();
23
+ });
24
+ },
25
+ });
26
+
27
+ return new Response(stream, {
28
+ headers: {
29
+ 'Content-Type': 'text/event-stream',
30
+ 'Cache-Control': 'no-cache',
31
+ 'Connection': 'keep-alive',
32
+ 'X-Accel-Buffering': 'no',
33
+ },
34
+ });
35
+ }
@@ -0,0 +1,42 @@
1
+ import { handleMessages } from '@/lib/proxy/handlers';
2
+
3
+ /**
4
+ * 代理服务器入口
5
+ * 拦截 /v1/messages 请求并记录
6
+ */
7
+ export async function POST(request: Request) {
8
+ try {
9
+ // 提取请求头和 body
10
+ const headers: Record<string, string> = {};
11
+ request.headers.forEach((value, key) => {
12
+ headers[key] = value;
13
+ });
14
+
15
+ const body = await request.json();
16
+
17
+ // 转发到处理器
18
+ return handleMessages(request, body, headers);
19
+ } catch (error: any) {
20
+ console.error('Proxy error:', error);
21
+ return new Response(
22
+ JSON.stringify({ error: error?.message || 'Proxy error' }),
23
+ {
24
+ status: 500,
25
+ headers: { 'Content-Type': 'application/json' },
26
+ }
27
+ );
28
+ }
29
+ }
30
+
31
+ /**
32
+ * GET 请求用于健康检查
33
+ */
34
+ export async function GET() {
35
+ return new Response(
36
+ JSON.stringify({ status: 'ok', message: 'CC Inspector Proxy Running' }),
37
+ {
38
+ status: 200,
39
+ headers: { 'Content-Type': 'application/json' },
40
+ }
41
+ );
42
+ }
@@ -0,0 +1,82 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getStore } from '@/lib/recorder';
3
+
4
+ /**
5
+ * 获取单个请求的详细信息
6
+ */
7
+ export async function GET(
8
+ request: Request,
9
+ { params }: { params: Promise<{ id: string }> }
10
+ ) {
11
+ try {
12
+ const { id } = await params;
13
+ const req = getStore().findById(id);
14
+
15
+ if (!req) {
16
+ return NextResponse.json({ error: 'Request not found' }, { status: 404 });
17
+ }
18
+
19
+ // 解析 JSON 字符串
20
+ let requestBody = null;
21
+ let responseBody = null;
22
+ let requestHeaders = null;
23
+ let responseHeaders = null;
24
+ let streamingEvents = null;
25
+
26
+ try {
27
+ requestBody = req.request_body ? JSON.parse(req.request_body) : null;
28
+ } catch {
29
+ requestBody = req.request_body;
30
+ }
31
+
32
+ try {
33
+ responseBody = req.response_body ? JSON.parse(req.response_body) : null;
34
+ } catch {
35
+ responseBody = req.response_body;
36
+ }
37
+
38
+ try {
39
+ requestHeaders = req.request_headers ? JSON.parse(req.request_headers) : null;
40
+ } catch {
41
+ requestHeaders = req.request_headers;
42
+ }
43
+
44
+ try {
45
+ responseHeaders = req.response_headers ? JSON.parse(req.response_headers) : null;
46
+ } catch {
47
+ responseHeaders = req.response_headers;
48
+ }
49
+
50
+ try {
51
+ streamingEvents = req.streaming_events ? JSON.parse(req.streaming_events) : null;
52
+ } catch {
53
+ streamingEvents = req.streaming_events;
54
+ }
55
+
56
+ return NextResponse.json({
57
+ id: req.id,
58
+ session_id: req.session_id,
59
+ endpoint: req.endpoint,
60
+ method: req.method,
61
+ request_headers: requestHeaders,
62
+ request_body: requestBody,
63
+ response_status: req.response_status,
64
+ response_headers: responseHeaders,
65
+ response_body: responseBody,
66
+ streaming_events: streamingEvents,
67
+ input_tokens: req.input_tokens,
68
+ output_tokens: req.output_tokens,
69
+ cache_read_tokens: req.cache_read_tokens,
70
+ cache_creation_tokens: req.cache_creation_tokens,
71
+ latency_ms: req.latency_ms,
72
+ first_token_ms: req.first_token_ms,
73
+ model: req.model,
74
+ cost_usd: req.cost_usd,
75
+ error_message: req.error_message,
76
+ created_at: req.created_at,
77
+ });
78
+ } catch (error: any) {
79
+ console.error('Failed to fetch request:', error);
80
+ return NextResponse.json({ error: error?.message || 'Failed to fetch request' }, { status: 500 });
81
+ }
82
+ }
@@ -0,0 +1,124 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getStore } from '@/lib/recorder';
3
+
4
+ /**
5
+ * 导出请求数据为 JSON 或 CSV 格式
6
+ */
7
+ export async function GET(request: Request) {
8
+ try {
9
+ const { searchParams } = new URL(request.url);
10
+ const format = searchParams.get('format') || 'json';
11
+ const ids = searchParams.get('ids'); // 可选,导出指定 ID 的请求
12
+
13
+ let requests: any[];
14
+
15
+ if (ids) {
16
+ // 导出指定 ID 的请求
17
+ const idList = ids.split(',');
18
+ requests = idList.map((id) => getStore().findById(id.trim())).filter(Boolean) as any[];
19
+ } else {
20
+ // 导出最近的请求
21
+ requests = getStore().getRecentRequests(500);
22
+ }
23
+
24
+ // 转换为前端友好的格式
25
+ const formattedRequests = requests.map((r) => ({
26
+ id: r.id,
27
+ session_id: r.session_id,
28
+ endpoint: r.endpoint,
29
+ method: r.method,
30
+ response_status: r.status_code,
31
+ input_tokens: r.input_tokens,
32
+ output_tokens: r.output_tokens,
33
+ cache_read_tokens: r.cache_read_tokens,
34
+ cache_creation_tokens: r.cache_creation_tokens,
35
+ latency_ms: r.latency_ms,
36
+ first_token_ms: r.first_token_ms,
37
+ model: r.model,
38
+ cost_usd: r.cost_usd,
39
+ created_at: r.created_at,
40
+ }));
41
+
42
+ if (format === 'csv') {
43
+ return exportToCSV(formattedRequests);
44
+ } else {
45
+ return exportToJSON(formattedRequests);
46
+ }
47
+ } catch (error: any) {
48
+ console.error('Failed to export requests:', error);
49
+ return NextResponse.json({ error: error?.message || 'Failed to export requests' }, { status: 500 });
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 导出为 JSON 格式
55
+ */
56
+ function exportToJSON(requests: any[]) {
57
+ const jsonContent = JSON.stringify(requests, null, 2);
58
+
59
+ return new NextResponse(jsonContent, {
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ 'Content-Disposition': 'attachment; filename="cc-inspector-requests.json"',
63
+ },
64
+ });
65
+ }
66
+
67
+ /**
68
+ * 导出为 CSV 格式
69
+ */
70
+ function exportToCSV(requests: any[]) {
71
+ const headers = [
72
+ 'id',
73
+ 'session_id',
74
+ 'endpoint',
75
+ 'method',
76
+ 'response_status',
77
+ 'input_tokens',
78
+ 'output_tokens',
79
+ 'cache_read_tokens',
80
+ 'cache_creation_tokens',
81
+ 'latency_ms',
82
+ 'first_token_ms',
83
+ 'model',
84
+ 'cost_usd',
85
+ 'created_at',
86
+ ];
87
+
88
+ const rows = requests.map((r) =>
89
+ [
90
+ r.id,
91
+ r.session_id || '',
92
+ r.endpoint,
93
+ r.method,
94
+ r.response_status || '',
95
+ r.input_tokens,
96
+ r.output_tokens,
97
+ r.cache_read_tokens,
98
+ r.cache_creation_tokens,
99
+ r.latency_ms || '',
100
+ r.first_token_ms || '',
101
+ r.model || '',
102
+ r.cost_usd || '',
103
+ r.created_at,
104
+ ]
105
+ .map((field) => {
106
+ // 处理包含逗号或引号的字段
107
+ const str = String(field);
108
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
109
+ return `"${str.replace(/"/g, '""')}"`;
110
+ }
111
+ return str;
112
+ })
113
+ .join(',')
114
+ );
115
+
116
+ const csvContent = [headers.join(','), ...rows].join('\n');
117
+
118
+ return new NextResponse(csvContent, {
119
+ headers: {
120
+ 'Content-Type': 'text/csv',
121
+ 'Content-Disposition': 'attachment; filename="cc-inspector-requests.csv"',
122
+ },
123
+ });
124
+ }
@@ -0,0 +1,32 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getStore } from '@/lib/recorder';
3
+
4
+ /**
5
+ * 获取最近的请求列表
6
+ */
7
+ export async function GET() {
8
+ try {
9
+ const requests = getStore().getRecentRequests(100);
10
+ // 转换为前端需要的格式
11
+ const formattedRequests = requests.map((r: any) => ({
12
+ id: r.id,
13
+ session_id: r.session_id,
14
+ endpoint: r.endpoint,
15
+ method: r.method,
16
+ response_status: r.status_code,
17
+ input_tokens: r.input_tokens,
18
+ output_tokens: r.output_tokens,
19
+ cache_read_tokens: r.cache_read_tokens,
20
+ cache_creation_tokens: r.cache_creation_tokens,
21
+ latency_ms: r.latency_ms,
22
+ first_token_ms: r.first_token_ms,
23
+ model: r.model,
24
+ cost_usd: r.cost_usd,
25
+ created_at: r.created_at,
26
+ }));
27
+ return NextResponse.json(formattedRequests);
28
+ } catch (error: any) {
29
+ console.error('Failed to fetch requests:', error);
30
+ return NextResponse.json({ error: error?.message || 'Failed to fetch requests' }, { status: 500 });
31
+ }
32
+ }