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.
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/publish-npm.yml +33 -0
- package/README.md +199 -0
- package/app/api/events/route.ts +35 -0
- package/app/api/proxy/route.ts +42 -0
- package/app/api/requests/[id]/route.ts +82 -0
- package/app/api/requests/export/route.ts +124 -0
- package/app/api/requests/route.ts +32 -0
- package/app/dashboard/page.tsx +562 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +26 -0
- package/app/layout.tsx +34 -0
- package/app/page.tsx +5 -0
- package/app/v1/messages/route.ts +30 -0
- package/components/JsonModal.tsx +155 -0
- package/components/JsonViewer.tsx +185 -0
- package/dev.sh +19 -0
- package/eslint.config.mjs +18 -0
- package/lib/env.ts +52 -0
- package/lib/pricing.ts +131 -0
- package/lib/proxy/forwarder.test.ts +171 -0
- package/lib/proxy/forwarder.ts +96 -0
- package/lib/proxy/handlers.test.ts +276 -0
- package/lib/proxy/handlers.ts +340 -0
- package/lib/proxy/ws-server.ts +76 -0
- package/lib/recorder/index.ts +152 -0
- package/lib/recorder/schema.ts +41 -0
- package/lib/recorder/store.ts +141 -0
- package/next.config.ts +59 -0
- package/package.json +42 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server.ts +64 -0
- package/tsconfig.json +34 -0
- package/tsconfig.server.json +11 -0
- package/vitest.config.ts +14 -0
|
@@ -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
|
+
}
|