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/LICENSE.md +21 -0
- package/README.md +66 -0
- package/cli.js +39 -0
- package/interceptor.js +341 -0
- package/lib/app.js +753 -0
- package/lib/index.html +57 -0
- package/lib/server.js +222 -0
- package/lib/style.css +824 -0
- package/lib/vendor/json-viewer.bundle.js +1289 -0
- package/lib/vendor/marked.umd.js +74 -0
- package/package.json +46 -0
package/lib/app.js
ADDED
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
let requests = [];
|
|
2
|
+
let selectedIndex = null;
|
|
3
|
+
let currentTab = 'request';
|
|
4
|
+
let collapsedSections = {
|
|
5
|
+
requestHeaders: true,
|
|
6
|
+
responseHeaders: true
|
|
7
|
+
};
|
|
8
|
+
let bodyViewMode = { request: 'json', response: 'json' };
|
|
9
|
+
let viewMode = 'raw';
|
|
10
|
+
let _autoSelectTimer = null;
|
|
11
|
+
|
|
12
|
+
// 使用第三方库渲染 JSON
|
|
13
|
+
function renderJson(data, containerId) {
|
|
14
|
+
try {
|
|
15
|
+
const container = document.getElementById(containerId);
|
|
16
|
+
if (!container) {
|
|
17
|
+
console.error('找不到容器:', containerId);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 清空容器
|
|
22
|
+
container.innerHTML = '';
|
|
23
|
+
|
|
24
|
+
// 判断根类型的括号
|
|
25
|
+
const isArray = Array.isArray(data);
|
|
26
|
+
const isObject = data !== null && typeof data === 'object';
|
|
27
|
+
const openBracket = isArray ? '[' : (isObject ? '{' : '');
|
|
28
|
+
const closeBracket = isArray ? ']' : (isObject ? '}' : '');
|
|
29
|
+
|
|
30
|
+
// 等待 json-viewer 组件定义完成
|
|
31
|
+
customElements.whenDefined('json-viewer').then(() => {
|
|
32
|
+
// 添加根级开括号
|
|
33
|
+
if (openBracket) {
|
|
34
|
+
const open = document.createElement('span');
|
|
35
|
+
open.textContent = openBracket;
|
|
36
|
+
open.style.cssText = 'color: #e5e7eb; font-family: var(--font-family, monospace); font-size: 13px; display: block; line-height: 1.5rem;';
|
|
37
|
+
container.appendChild(open);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 创建 json-viewer 元素
|
|
41
|
+
const viewer = document.createElement('json-viewer');
|
|
42
|
+
viewer.data = data;
|
|
43
|
+
|
|
44
|
+
// 添加到容器
|
|
45
|
+
container.appendChild(viewer);
|
|
46
|
+
|
|
47
|
+
requestAnimationFrame(() => {
|
|
48
|
+
try {
|
|
49
|
+
if (containerId === 'response-body-viewer') {
|
|
50
|
+
// response body 默认全部展开
|
|
51
|
+
viewer.expandAll();
|
|
52
|
+
} else if (containerId === 'request-body-viewer') {
|
|
53
|
+
// request body 中 messages/system/tools 展开一层
|
|
54
|
+
viewer.setState((state) => ({
|
|
55
|
+
expanded: {
|
|
56
|
+
...state.expanded,
|
|
57
|
+
messages: true,
|
|
58
|
+
system: true,
|
|
59
|
+
tools: true
|
|
60
|
+
}
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 添加根级闭括号
|
|
67
|
+
if (closeBracket) {
|
|
68
|
+
const close = document.createElement('span');
|
|
69
|
+
close.textContent = closeBracket;
|
|
70
|
+
close.style.cssText = 'color: #e5e7eb; font-family: var(--font-family, monospace); font-size: 13px; display: block; line-height: 1.5rem;';
|
|
71
|
+
container.appendChild(close);
|
|
72
|
+
}
|
|
73
|
+
}).catch(error => {
|
|
74
|
+
console.error('json-viewer 组件加载失败:', error);
|
|
75
|
+
container.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('JSON渲染错误:', error);
|
|
79
|
+
const container = document.getElementById(containerId);
|
|
80
|
+
if (container) {
|
|
81
|
+
container.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre>`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function escapeHtml(str) {
|
|
87
|
+
const div = document.createElement('div');
|
|
88
|
+
div.textContent = str;
|
|
89
|
+
return div.innerHTML;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 将 markdown 文本渲染为 HTML(用于 assistant/subAgent 回复)
|
|
93
|
+
function renderMarkdown(text) {
|
|
94
|
+
if (!text) return '';
|
|
95
|
+
if (typeof marked !== 'undefined' && marked.parse) {
|
|
96
|
+
try {
|
|
97
|
+
return marked.parse(text, { breaks: true });
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return escapeHtml(text);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return escapeHtml(text);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 已知的系统注入标签名(出现在 assistant text 中需要折叠展示)
|
|
106
|
+
const SYSTEM_TAGS = [
|
|
107
|
+
'system-reminder', 'local-command-caveat', 'project-reminder',
|
|
108
|
+
'important-instruction-reminders', 'file-modified-reminder', 'todo-reminder',
|
|
109
|
+
'user-prompt-submit-hook', 'local-command-stdout', 'command-name',
|
|
110
|
+
'task-notification', 'environment_details', 'context'
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
// 从文本中提取系统标签块,返回 { segments: [{type, tag?, content}] }
|
|
114
|
+
function parseSystemTags(text) {
|
|
115
|
+
if (!text) return { segments: [] };
|
|
116
|
+
const tagPattern = new RegExp(
|
|
117
|
+
'<(' + SYSTEM_TAGS.join('|') + ')\\b[^>]*>([\\s\\S]*?)</\\1>',
|
|
118
|
+
'gi'
|
|
119
|
+
);
|
|
120
|
+
const segments = [];
|
|
121
|
+
let lastIndex = 0;
|
|
122
|
+
let match;
|
|
123
|
+
while ((match = tagPattern.exec(text)) !== null) {
|
|
124
|
+
if (match.index > lastIndex) {
|
|
125
|
+
const before = text.slice(lastIndex, match.index).trim();
|
|
126
|
+
if (before) segments.push({ type: 'text', content: before });
|
|
127
|
+
}
|
|
128
|
+
segments.push({ type: 'system-tag', tag: match[1], content: match[2].trim() });
|
|
129
|
+
lastIndex = match.index + match[0].length;
|
|
130
|
+
}
|
|
131
|
+
if (lastIndex < text.length) {
|
|
132
|
+
const rest = text.slice(lastIndex).trim();
|
|
133
|
+
if (rest) segments.push({ type: 'text', content: rest });
|
|
134
|
+
}
|
|
135
|
+
return { segments };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 渲染 assistant 文本:系统标签折叠,其余走 markdown
|
|
139
|
+
function renderAssistantText(text) {
|
|
140
|
+
const { segments } = parseSystemTags(text);
|
|
141
|
+
if (segments.length === 0) return '';
|
|
142
|
+
let html = '';
|
|
143
|
+
for (const seg of segments) {
|
|
144
|
+
if (seg.type === 'system-tag') {
|
|
145
|
+
html += `<details class="chat-thinking"><summary>${escapeHtml(seg.tag)}</summary><div class="chat-thinking-content">${escapeHtml(seg.content)}</div></details>`;
|
|
146
|
+
} else {
|
|
147
|
+
html += `<div class="chat-md">${renderMarkdown(seg.content)}</div>`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return html;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 等待DOM加载完成后初始化
|
|
154
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
155
|
+
initializeApp();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
function initializeApp() {
|
|
159
|
+
// 拖拽调整宽度功能
|
|
160
|
+
const leftPanel = document.getElementById('left-panel');
|
|
161
|
+
const resizer = document.getElementById('resizer');
|
|
162
|
+
|
|
163
|
+
if (!leftPanel || !resizer) {
|
|
164
|
+
console.error('无法找到必要的DOM元素');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let isResizing = false;
|
|
169
|
+
|
|
170
|
+
resizer.addEventListener('mousedown', (e) => {
|
|
171
|
+
isResizing = true;
|
|
172
|
+
document.body.style.cursor = 'col-resize';
|
|
173
|
+
document.body.style.userSelect = 'none';
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
document.addEventListener('mousemove', (e) => {
|
|
177
|
+
if (!isResizing) return;
|
|
178
|
+
|
|
179
|
+
const containerRect = document.querySelector('.main-container').getBoundingClientRect();
|
|
180
|
+
const newWidth = e.clientX - containerRect.left;
|
|
181
|
+
|
|
182
|
+
// 限制最小和最大宽度
|
|
183
|
+
if (newWidth >= 250 && newWidth <= 800) {
|
|
184
|
+
leftPanel.style.width = newWidth + 'px';
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
document.addEventListener('mouseup', () => {
|
|
189
|
+
if (isResizing) {
|
|
190
|
+
isResizing = false;
|
|
191
|
+
document.body.style.cursor = '';
|
|
192
|
+
document.body.style.userSelect = '';
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// 初始化EventSource连接
|
|
197
|
+
initializeEventSource();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function initializeEventSource() {
|
|
201
|
+
try {
|
|
202
|
+
const eventSource = new EventSource('/events');
|
|
203
|
+
|
|
204
|
+
eventSource.onmessage = handleEventMessage;
|
|
205
|
+
eventSource.onerror = handleEventError;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('EventSource初始化失败:', error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handleEventMessage(event) {
|
|
212
|
+
try {
|
|
213
|
+
const entry = JSON.parse(event.data);
|
|
214
|
+
|
|
215
|
+
// 直接添加新的请求条目
|
|
216
|
+
const existingIndex = requests.findIndex(r =>
|
|
217
|
+
r.timestamp === entry.timestamp && r.url === entry.url
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (existingIndex >= 0) {
|
|
221
|
+
requests[existingIndex] = entry;
|
|
222
|
+
} else {
|
|
223
|
+
requests.push(entry);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
renderRequests();
|
|
227
|
+
|
|
228
|
+
// 没有选中状态时,等初始数据加载完后选中最后一条
|
|
229
|
+
if (selectedIndex === null && requests.length > 0) {
|
|
230
|
+
clearTimeout(_autoSelectTimer);
|
|
231
|
+
_autoSelectTimer = setTimeout(() => {
|
|
232
|
+
if (selectedIndex === null && requests.length > 0) {
|
|
233
|
+
selectedIndex = requests.length - 1;
|
|
234
|
+
renderRequests();
|
|
235
|
+
renderDetail();
|
|
236
|
+
const list = document.getElementById('request-list');
|
|
237
|
+
if (list) list.scrollTop = list.scrollHeight;
|
|
238
|
+
}
|
|
239
|
+
}, 200);
|
|
240
|
+
} else if (selectedIndex === requests.length - 1) {
|
|
241
|
+
// 如果当前选中的是最新的请求,自动更新详情
|
|
242
|
+
renderDetail();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 对话模式下自动刷新
|
|
246
|
+
if (viewMode === 'chat') {
|
|
247
|
+
renderChatView();
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error('处理事件消息失败:', error);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function handleEventError() {
|
|
255
|
+
console.error('SSE连接错误');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
function renderRequests() {
|
|
260
|
+
const container = document.getElementById('request-list');
|
|
261
|
+
document.getElementById('total-count').textContent = requests.length;
|
|
262
|
+
|
|
263
|
+
if (requests.length === 0) {
|
|
264
|
+
container.innerHTML = `
|
|
265
|
+
<div class="empty-state">
|
|
266
|
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
267
|
+
<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>
|
|
268
|
+
</svg>
|
|
269
|
+
<p>等待请求...</p>
|
|
270
|
+
</div>
|
|
271
|
+
`;
|
|
272
|
+
selectedIndex = null;
|
|
273
|
+
renderDetail();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
container.innerHTML = requests.map((req, index) => {
|
|
278
|
+
const time = new Date(req.timestamp).toLocaleTimeString('zh-CN');
|
|
279
|
+
const urlObj = new URL(req.url);
|
|
280
|
+
const urlPreview = urlObj.pathname + urlObj.search;
|
|
281
|
+
|
|
282
|
+
const statusClass = req.response
|
|
283
|
+
? (req.response.status < 400 ? 'status-success' : 'status-error')
|
|
284
|
+
: '';
|
|
285
|
+
const statusText = req.response ? `HTTP ${req.response.status}` : '';
|
|
286
|
+
|
|
287
|
+
return `
|
|
288
|
+
<div class="request-item ${index === selectedIndex ? 'active' : ''} ${req.mainAgent ? 'main-agent' : ''}" onclick="selectRequest(${index})">
|
|
289
|
+
<div class="request-item-header">
|
|
290
|
+
<span class="method ${req.method}">${req.method}</span>
|
|
291
|
+
${req.mainAgent ? '<span class="agent-badge main">MainAgent</span>' : '<span class="agent-badge sub">SubAgent</span>'}
|
|
292
|
+
<span class="timestamp">${time}</span>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="url-preview" title="${req.url}">${urlPreview}</div>
|
|
295
|
+
${req.duration || req.response ? `
|
|
296
|
+
<div class="request-meta">
|
|
297
|
+
${req.duration ? `<span>⏱️ ${req.duration}ms</span>` : ''}
|
|
298
|
+
${req.response ? `<span class="${statusClass}">${statusText}</span>` : ''}
|
|
299
|
+
</div>
|
|
300
|
+
` : ''}
|
|
301
|
+
</div>
|
|
302
|
+
`;
|
|
303
|
+
}).join('');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function selectRequest(index) {
|
|
307
|
+
selectedIndex = index;
|
|
308
|
+
renderRequests();
|
|
309
|
+
renderDetail();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function renderDetail() {
|
|
313
|
+
const container = document.getElementById('right-panel');
|
|
314
|
+
|
|
315
|
+
if (selectedIndex === null || !requests[selectedIndex]) {
|
|
316
|
+
container.innerHTML = `
|
|
317
|
+
<div class="empty-detail">
|
|
318
|
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
319
|
+
<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>
|
|
320
|
+
</svg>
|
|
321
|
+
<p>选择一个请求查看详情</p>
|
|
322
|
+
</div>
|
|
323
|
+
`;
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const req = requests[selectedIndex];
|
|
328
|
+
const time = new Date(req.timestamp).toLocaleString('zh-CN');
|
|
329
|
+
|
|
330
|
+
// Build headers tables
|
|
331
|
+
const requestHeadersTable = req.headers
|
|
332
|
+
? Object.entries(req.headers).map(([key, value]) =>
|
|
333
|
+
`<tr><td>${key}</td><td>${value}</td></tr>`
|
|
334
|
+
).join('')
|
|
335
|
+
: '<tr><td colspan="2">无 Headers</td></tr>';
|
|
336
|
+
|
|
337
|
+
const responseHeadersTable = req.response?.headers
|
|
338
|
+
? Object.entries(req.response.headers).map(([key, value]) =>
|
|
339
|
+
`<tr><td>${key}</td><td>${value}</td></tr>`
|
|
340
|
+
).join('')
|
|
341
|
+
: '<tr><td colspan="2">无 Headers</td></tr>';
|
|
342
|
+
|
|
343
|
+
container.innerHTML = `
|
|
344
|
+
<div class="detail-header">
|
|
345
|
+
<div class="detail-url">${req.url}</div>
|
|
346
|
+
<div class="detail-meta">
|
|
347
|
+
<div class="detail-meta-item">
|
|
348
|
+
<span class="method ${req.method}">${req.method}</span>
|
|
349
|
+
</div>
|
|
350
|
+
<div class="detail-meta-item">
|
|
351
|
+
<span>🕐 ${time}</span>
|
|
352
|
+
</div>
|
|
353
|
+
${req.duration ? `
|
|
354
|
+
<div class="detail-meta-item">
|
|
355
|
+
<span>⏱️ ${req.duration}ms</span>
|
|
356
|
+
</div>
|
|
357
|
+
` : ''}
|
|
358
|
+
${req.response ? `
|
|
359
|
+
<div class="detail-meta-item">
|
|
360
|
+
<span class="${req.response.status < 400 ? 'status-success' : 'status-error'}">
|
|
361
|
+
HTTP ${req.response.status}
|
|
362
|
+
</span>
|
|
363
|
+
</div>
|
|
364
|
+
` : ''}
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<div class="tabs">
|
|
369
|
+
<div class="tab ${currentTab === 'request' ? 'active' : ''}" onclick="switchTab('request')">
|
|
370
|
+
Request
|
|
371
|
+
</div>
|
|
372
|
+
<div class="tab ${currentTab === 'response' ? 'active' : ''}" onclick="switchTab('response')">
|
|
373
|
+
Response
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<div class="tab-content">
|
|
378
|
+
<div class="tab-pane ${currentTab === 'request' ? 'active' : ''}">
|
|
379
|
+
<div class="info-section">
|
|
380
|
+
<div class="info-title collapsible" onclick="toggleSection('requestHeaders')">
|
|
381
|
+
<span>${collapsedSections.requestHeaders ? '▶' : '▼'} Headers</span>
|
|
382
|
+
</div>
|
|
383
|
+
${!collapsedSections.requestHeaders ? `
|
|
384
|
+
<table class="info-table">
|
|
385
|
+
${requestHeadersTable}
|
|
386
|
+
</table>
|
|
387
|
+
` : ''}
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
${req.body ? `
|
|
391
|
+
<div class="info-section">
|
|
392
|
+
<div class="info-title body-title-bar">
|
|
393
|
+
<span>Body</span>
|
|
394
|
+
<div class="body-actions">
|
|
395
|
+
<button class="body-action-btn ${bodyViewMode.request === 'text' ? 'active' : ''}" onclick="toggleBodyViewMode('request')" title="${bodyViewMode.request === 'json' ? '切换为纯文本' : '切换为 JSON 视图'}">
|
|
396
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
|
397
|
+
<span>${bodyViewMode.request === 'json' ? 'Text' : 'JSON'}</span>
|
|
398
|
+
</button>
|
|
399
|
+
<button class="body-action-btn" onclick="copyBodyJson('request')" title="复制 JSON">
|
|
400
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
401
|
+
<span>复制</span>
|
|
402
|
+
</button>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
${bodyViewMode.request === 'json'
|
|
406
|
+
? '<div class="json-viewer" id="request-body-viewer"></div>'
|
|
407
|
+
: `<pre class="body-text-view">${escapeHtml(typeof req.body === 'string' ? req.body : JSON.stringify(req.body, null, 2))}</pre>`}
|
|
408
|
+
</div>
|
|
409
|
+
` : '<div class="info-section"><div class="info-title">Body</div><p style="color: #6b7280;">无 Body</p></div>'}
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
<div class="tab-pane ${currentTab === 'response' ? 'active' : ''}">
|
|
413
|
+
${req.response ? `
|
|
414
|
+
<div class="info-section">
|
|
415
|
+
<div class="info-title collapsible" onclick="toggleSection('responseHeaders')">
|
|
416
|
+
<span>${collapsedSections.responseHeaders ? '▶' : '▼'} Headers</span>
|
|
417
|
+
</div>
|
|
418
|
+
${!collapsedSections.responseHeaders ? `
|
|
419
|
+
<table class="info-table">
|
|
420
|
+
${responseHeadersTable}
|
|
421
|
+
</table>
|
|
422
|
+
` : ''}
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<div class="info-section">
|
|
426
|
+
<div class="info-title body-title-bar">
|
|
427
|
+
<span>Body</span>
|
|
428
|
+
${!(typeof req.response.body === 'string' && req.response.body.includes('Streaming Response')) ? `
|
|
429
|
+
<div class="body-actions">
|
|
430
|
+
<button class="body-action-btn ${bodyViewMode.response === 'text' ? 'active' : ''}" onclick="toggleBodyViewMode('response')" title="${bodyViewMode.response === 'json' ? '切换为纯文本' : '切换为 JSON 视图'}">
|
|
431
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
|
432
|
+
<span>${bodyViewMode.response === 'json' ? 'Text' : 'JSON'}</span>
|
|
433
|
+
</button>
|
|
434
|
+
<button class="body-action-btn" onclick="copyBodyJson('response')" title="复制 JSON">
|
|
435
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
436
|
+
<span>复制</span>
|
|
437
|
+
</button>
|
|
438
|
+
</div>
|
|
439
|
+
` : ''}
|
|
440
|
+
</div>
|
|
441
|
+
${typeof req.response.body === 'string' && req.response.body.includes('Streaming Response')
|
|
442
|
+
? '<div style="padding: 20px; background: #1a1a1a; border-radius: 6px; border: 1px solid #2a2a2a; color: #9ca3af;"><p>⚡ 流式响应</p><p style="margin-top: 8px; font-size: 13px;">此请求使用了流式传输(SSE),响应内容无法完整捕获。</p></div>'
|
|
443
|
+
: bodyViewMode.response === 'json'
|
|
444
|
+
? '<div class="json-viewer" id="response-body-viewer"></div>'
|
|
445
|
+
: `<pre class="body-text-view">${escapeHtml(typeof req.response.body === 'string' ? req.response.body : JSON.stringify(req.response.body, null, 2))}</pre>`}
|
|
446
|
+
</div>
|
|
447
|
+
` : '<div class="empty-detail"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width: 48px; height: 48px; margin-bottom: 12px; opacity: 0.3;"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg><p>响应数据未捕获</p></div>'}
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
`;
|
|
451
|
+
|
|
452
|
+
// 渲染完 HTML 后,再渲染 JSON
|
|
453
|
+
if (currentTab === 'request' && req.body && bodyViewMode.request === 'json') {
|
|
454
|
+
renderJson(req.body, 'request-body-viewer');
|
|
455
|
+
} else if (currentTab === 'response' && req.response && bodyViewMode.response === 'json' && !(typeof req.response.body === 'string' && req.response.body.includes('Streaming Response'))) {
|
|
456
|
+
renderJson(req.response.body, 'response-body-viewer');
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function switchTab(tab) {
|
|
461
|
+
currentTab = tab;
|
|
462
|
+
renderDetail();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function toggleSection(section) {
|
|
466
|
+
collapsedSections[section] = !collapsedSections[section];
|
|
467
|
+
renderDetail();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function clearRequests() {
|
|
471
|
+
if (confirm('确定要清空所有请求记录吗?')) {
|
|
472
|
+
requests = [];
|
|
473
|
+
selectedIndex = null;
|
|
474
|
+
renderRequests();
|
|
475
|
+
renderDetail();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function showToast(message) {
|
|
480
|
+
let toast = document.getElementById('copy-toast');
|
|
481
|
+
if (!toast) {
|
|
482
|
+
toast = document.createElement('div');
|
|
483
|
+
toast.id = 'copy-toast';
|
|
484
|
+
document.body.appendChild(toast);
|
|
485
|
+
}
|
|
486
|
+
toast.textContent = message;
|
|
487
|
+
toast.classList.add('show');
|
|
488
|
+
setTimeout(() => toast.classList.remove('show'), 2000);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function copyBodyJson(type) {
|
|
492
|
+
const req = requests[selectedIndex];
|
|
493
|
+
if (!req) return;
|
|
494
|
+
const data = type === 'request' ? req.body : req.response?.body;
|
|
495
|
+
if (data == null) return;
|
|
496
|
+
const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
497
|
+
navigator.clipboard.writeText(text).then(() => showToast('复制成功'));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function toggleBodyViewMode(type) {
|
|
501
|
+
bodyViewMode[type] = bodyViewMode[type] === 'json' ? 'text' : 'json';
|
|
502
|
+
renderDetail();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ========== 对话模式 ==========
|
|
506
|
+
|
|
507
|
+
function toggleViewMode() {
|
|
508
|
+
viewMode = viewMode === 'raw' ? 'chat' : 'raw';
|
|
509
|
+
const btn = document.getElementById('mode-toggle-btn');
|
|
510
|
+
const mainContainer = document.querySelector('.main-container');
|
|
511
|
+
const chatContainer = document.getElementById('chat-container');
|
|
512
|
+
|
|
513
|
+
if (viewMode === 'chat') {
|
|
514
|
+
btn.textContent = '打开原文模式';
|
|
515
|
+
mainContainer.style.display = 'none';
|
|
516
|
+
chatContainer.style.display = 'flex';
|
|
517
|
+
renderChatView();
|
|
518
|
+
} else {
|
|
519
|
+
btn.textContent = '打开对话模式';
|
|
520
|
+
mainContainer.style.display = 'flex';
|
|
521
|
+
chatContainer.style.display = 'none';
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function getSvgAvatar(type) {
|
|
526
|
+
if (type === 'user') {
|
|
527
|
+
return '<svg viewBox="0 0 24 24"><path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/></svg>';
|
|
528
|
+
}
|
|
529
|
+
if (type === 'agent') {
|
|
530
|
+
return '<svg viewBox="0 0 24 24"><path d="M12 2a2 2 0 012 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 017 7h1a1 1 0 011 1v3a1 1 0 01-1 1h-1.17A7 7 0 0113 22h-2a7 7 0 01-6.83-3H3a1 1 0 01-1-1v-3a1 1 0 011-1h1a7 7 0 017-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 012-2zM9.5 13a1.5 1.5 0 100 3 1.5 1.5 0 000-3zm5 0a1.5 1.5 0 100 3 1.5 1.5 0 000-3z"/></svg>';
|
|
531
|
+
}
|
|
532
|
+
// sub-agent
|
|
533
|
+
return '<svg viewBox="0 0 24 24"><path d="M12 15.5A3.5 3.5 0 018.5 12 3.5 3.5 0 0112 8.5a3.5 3.5 0 013.5 3.5 3.5 3.5 0 01-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97s-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65A.49.49 0 0014 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1s.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.58 1.69-.98l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66z"/></svg>';
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function truncateText(text, maxLen) {
|
|
537
|
+
if (!text) return '';
|
|
538
|
+
return text.length > maxLen ? text.substring(0, maxLen) + '...' : text;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// 判断文本是否为系统注入内容(非用户原始输入)
|
|
542
|
+
function isSystemText(text) {
|
|
543
|
+
if (!text) return true;
|
|
544
|
+
const trimmed = text.trim();
|
|
545
|
+
if (!trimmed) return true;
|
|
546
|
+
// Claude Code 注入的系统内容都以 XML 标签开头,如 <system-reminder>, <task-notification>,
|
|
547
|
+
// <local-command-caveat>, <command-name>, <thinking_mode> 等
|
|
548
|
+
if (/^<[a-zA-Z_][\w-]*[\s>]/i.test(trimmed)) return true;
|
|
549
|
+
// [SUGGESTION MODE: ...] 等方括号开头的系统指令
|
|
550
|
+
if (/^\[SUGGESTION MODE:/i.test(trimmed)) return true;
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function renderChatView() {
|
|
555
|
+
const chatContainer = document.getElementById('chat-container');
|
|
556
|
+
|
|
557
|
+
// 找到最新的 mainAgent 请求
|
|
558
|
+
let mainReq = null;
|
|
559
|
+
for (let i = requests.length - 1; i >= 0; i--) {
|
|
560
|
+
if (requests[i].mainAgent === true) {
|
|
561
|
+
mainReq = requests[i];
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (!mainReq || !mainReq.body || !mainReq.body.messages) {
|
|
567
|
+
chatContainer.innerHTML = `
|
|
568
|
+
<div class="chat-empty">
|
|
569
|
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
570
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
|
571
|
+
</svg>
|
|
572
|
+
<p>暂无 mainAgent 对话数据</p>
|
|
573
|
+
</div>
|
|
574
|
+
`;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const messages = mainReq.body.messages;
|
|
579
|
+
|
|
580
|
+
// 构建 tool_use_id → tool_use 映射
|
|
581
|
+
const toolUseMap = {};
|
|
582
|
+
for (const msg of messages) {
|
|
583
|
+
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
584
|
+
for (const block of msg.content) {
|
|
585
|
+
if (block.type === 'tool_use') {
|
|
586
|
+
toolUseMap[block.id] = block;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
let html = '<div class="chat-messages">';
|
|
593
|
+
|
|
594
|
+
for (const msg of messages) {
|
|
595
|
+
const content = msg.content;
|
|
596
|
+
|
|
597
|
+
if (msg.role === 'user') {
|
|
598
|
+
if (Array.isArray(content)) {
|
|
599
|
+
// 检查是否有 tool_result
|
|
600
|
+
const toolResults = content.filter(b => b.type === 'tool_result');
|
|
601
|
+
const textBlocks = content.filter(b => b.type === 'text' && !isSystemText(b.text));
|
|
602
|
+
|
|
603
|
+
// 渲染 tool_result 为 subAgent 返回
|
|
604
|
+
for (const tr of toolResults) {
|
|
605
|
+
const matchedTool = toolUseMap[tr.tool_use_id];
|
|
606
|
+
let label = '工具返回';
|
|
607
|
+
if (matchedTool) {
|
|
608
|
+
if (matchedTool.name === 'Task' && matchedTool.input) {
|
|
609
|
+
const st = matchedTool.input.subagent_type || '';
|
|
610
|
+
const desc = matchedTool.input.description || '';
|
|
611
|
+
label = `SubAgent: ${st}${desc ? ' — ' + desc : ''}`;
|
|
612
|
+
} else {
|
|
613
|
+
label = matchedTool.name + ' 返回';
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const resultText = extractToolResultText(tr);
|
|
618
|
+
html += renderSubAgentBubble(label, resultText);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// 渲染用户文本
|
|
622
|
+
for (const tb of textBlocks) {
|
|
623
|
+
html += renderUserBubble(tb.text);
|
|
624
|
+
}
|
|
625
|
+
} else if (typeof content === 'string' && !isSystemText(content)) {
|
|
626
|
+
html += renderUserBubble(content);
|
|
627
|
+
}
|
|
628
|
+
} else if (msg.role === 'assistant') {
|
|
629
|
+
if (Array.isArray(content)) {
|
|
630
|
+
const thinkingBlocks = content.filter(b => b.type === 'thinking');
|
|
631
|
+
const textBlocks = content.filter(b => b.type === 'text');
|
|
632
|
+
const toolUseBlocks = content.filter(b => b.type === 'tool_use');
|
|
633
|
+
|
|
634
|
+
// 一个 assistant 消息可能包含多种 block,合并渲染
|
|
635
|
+
let bubbleInner = '';
|
|
636
|
+
|
|
637
|
+
for (const tb of thinkingBlocks) {
|
|
638
|
+
bubbleInner += `<details class="chat-thinking"><summary>思考过程</summary><div class="chat-thinking-content">${escapeHtml(tb.thinking || '')}</div></details>`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
for (const tb of textBlocks) {
|
|
642
|
+
if (tb.text) {
|
|
643
|
+
bubbleInner += renderAssistantText(tb.text);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
for (const tu of toolUseBlocks) {
|
|
648
|
+
let toolLabel = tu.name;
|
|
649
|
+
if (tu.name === 'Task' && tu.input) {
|
|
650
|
+
const st = tu.input.subagent_type || '';
|
|
651
|
+
const desc = tu.input.description || '';
|
|
652
|
+
toolLabel = `Task(${st}${desc ? ': ' + desc : ''})`;
|
|
653
|
+
}
|
|
654
|
+
const inputPreview = tu.input ? truncateText(JSON.stringify(tu.input), 300) : '';
|
|
655
|
+
bubbleInner += `<div class="chat-tool-call"><span class="tool-name">${escapeHtml(toolLabel)}</span><div class="tool-input">${escapeHtml(inputPreview)}</div></div>`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (bubbleInner) {
|
|
659
|
+
html += renderAssistantBubble(bubbleInner);
|
|
660
|
+
}
|
|
661
|
+
} else if (typeof content === 'string') {
|
|
662
|
+
html += renderAssistantBubble(renderAssistantText(content));
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// 渲染 response 中 assistant 的最终回复
|
|
668
|
+
if (mainReq.response && mainReq.response.body && mainReq.response.body.content) {
|
|
669
|
+
const respContent = mainReq.response.body.content;
|
|
670
|
+
if (Array.isArray(respContent)) {
|
|
671
|
+
let bubbleInner = '';
|
|
672
|
+
|
|
673
|
+
for (const block of respContent) {
|
|
674
|
+
if (block.type === 'thinking') {
|
|
675
|
+
bubbleInner += `<details class="chat-thinking"><summary>思考过程</summary><div class="chat-thinking-content">${escapeHtml(block.thinking || '')}</div></details>`;
|
|
676
|
+
} else if (block.type === 'text' && block.text) {
|
|
677
|
+
bubbleInner += renderAssistantText(block.text);
|
|
678
|
+
} else if (block.type === 'tool_use') {
|
|
679
|
+
let toolLabel = block.name;
|
|
680
|
+
if (block.name === 'Task' && block.input) {
|
|
681
|
+
const st = block.input.subagent_type || '';
|
|
682
|
+
const desc = block.input.description || '';
|
|
683
|
+
toolLabel = `Task(${st}${desc ? ': ' + desc : ''})`;
|
|
684
|
+
}
|
|
685
|
+
const inputPreview = block.input ? truncateText(JSON.stringify(block.input), 300) : '';
|
|
686
|
+
bubbleInner += `<div class="chat-tool-call"><span class="tool-name">${escapeHtml(toolLabel)}</span><div class="tool-input">${escapeHtml(inputPreview)}</div></div>`;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (bubbleInner) {
|
|
691
|
+
html += `<div style="border-top: 1px solid #2a2a2a; margin: 8px 0; padding-top: 8px;"><span style="font-size: 11px; color: #6b7280;">Response</span></div>`;
|
|
692
|
+
html += renderAssistantBubble(bubbleInner);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
html += '</div>';
|
|
698
|
+
chatContainer.innerHTML = html;
|
|
699
|
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function renderUserBubble(text) {
|
|
703
|
+
return `
|
|
704
|
+
<div class="chat-message user">
|
|
705
|
+
<div class="chat-avatar user-avatar">${getSvgAvatar('user')}</div>
|
|
706
|
+
<div>
|
|
707
|
+
<div class="chat-role-label">User</div>
|
|
708
|
+
<div class="chat-bubble">${escapeHtml(text)}</div>
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
711
|
+
`;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function renderAssistantBubble(innerHtml) {
|
|
715
|
+
return `
|
|
716
|
+
<div class="chat-message assistant">
|
|
717
|
+
<div class="chat-avatar agent-avatar">${getSvgAvatar('agent')}</div>
|
|
718
|
+
<div>
|
|
719
|
+
<div class="chat-role-label">MainAgent</div>
|
|
720
|
+
<div class="chat-bubble">${innerHtml}</div>
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
723
|
+
`;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function renderSubAgentBubble(label, resultText) {
|
|
727
|
+
return `
|
|
728
|
+
<div class="chat-message sub-agent">
|
|
729
|
+
<div class="chat-avatar sub-avatar">${getSvgAvatar('sub')}</div>
|
|
730
|
+
<div>
|
|
731
|
+
<div class="chat-role-label">${escapeHtml(label)}</div>
|
|
732
|
+
<div class="chat-bubble">
|
|
733
|
+
<div class="chat-tool-result">
|
|
734
|
+
<div class="tool-result-label">Result</div>
|
|
735
|
+
<div class="tool-result-content">${escapeHtml(truncateText(resultText, 1000))}</div>
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function extractToolResultText(toolResult) {
|
|
744
|
+
if (!toolResult.content) return String(toolResult.content ?? '');
|
|
745
|
+
if (typeof toolResult.content === 'string') return toolResult.content;
|
|
746
|
+
if (Array.isArray(toolResult.content)) {
|
|
747
|
+
return toolResult.content
|
|
748
|
+
.filter(b => b.type === 'text')
|
|
749
|
+
.map(b => b.text)
|
|
750
|
+
.join('\n');
|
|
751
|
+
}
|
|
752
|
+
return JSON.stringify(toolResult.content);
|
|
753
|
+
}
|