669 lines
22 KiB
HTML
669 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>DeepResearch 调试查看器</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: #0a0a0a;
|
||
color: #e0e0e0;
|
||
line-height: 1.6;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
h1 {
|
||
color: #00ff88;
|
||
margin-bottom: 20px;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.controls {
|
||
background: #1a1a1a;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
gap: 15px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.controls input, .controls select, .controls button {
|
||
padding: 8px 15px;
|
||
background: #2a2a2a;
|
||
border: 1px solid #3a3a3a;
|
||
color: #e0e0e0;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.controls input {
|
||
flex: 1;
|
||
min-width: 300px;
|
||
}
|
||
|
||
.controls button {
|
||
cursor: pointer;
|
||
background: #00ff88;
|
||
color: #000;
|
||
font-weight: bold;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.controls button:hover {
|
||
background: #00cc6a;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 5px;
|
||
margin-bottom: 20px;
|
||
border-bottom: 1px solid #3a3a3a;
|
||
}
|
||
|
||
.tab {
|
||
padding: 10px 20px;
|
||
background: #1a1a1a;
|
||
border: none;
|
||
color: #888;
|
||
cursor: pointer;
|
||
border-radius: 4px 4px 0 0;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.tab.active {
|
||
background: #2a2a2a;
|
||
color: #00ff88;
|
||
}
|
||
|
||
.log-container {
|
||
background: #1a1a1a;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.log-entry {
|
||
background: #2a2a2a;
|
||
padding: 15px;
|
||
margin-bottom: 15px;
|
||
border-radius: 6px;
|
||
border-left: 3px solid #00ff88;
|
||
}
|
||
|
||
.log-entry.error {
|
||
border-left-color: #ff4444;
|
||
}
|
||
|
||
.log-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
font-size: 12px;
|
||
color: #888;
|
||
}
|
||
|
||
.log-model {
|
||
color: #00ff88;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.log-method {
|
||
color: #4488ff;
|
||
}
|
||
|
||
.log-content {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.content-section {
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.content-label {
|
||
font-weight: bold;
|
||
color: #4488ff;
|
||
margin-bottom: 5px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.toggle-btn {
|
||
font-size: 12px;
|
||
padding: 2px 8px;
|
||
background: #3a3a3a;
|
||
border: none;
|
||
color: #888;
|
||
cursor: pointer;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.toggle-btn:hover {
|
||
background: #4a4a4a;
|
||
}
|
||
|
||
.content-box {
|
||
background: #1a1a1a;
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
font-family: 'Consolas', 'Monaco', monospace;
|
||
font-size: 13px;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
overflow-x: auto;
|
||
max-height: none;
|
||
transition: max-height 0.3s ease;
|
||
}
|
||
|
||
.content-box.collapsed {
|
||
max-height: 200px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.content-box.collapsed::after {
|
||
content: "... (点击展开查看更多)";
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
padding: 20px;
|
||
background: linear-gradient(transparent, #1a1a1a);
|
||
text-align: center;
|
||
color: #888;
|
||
}
|
||
|
||
.prompt {
|
||
border-left: 3px solid #4488ff;
|
||
}
|
||
|
||
.response {
|
||
border-left: 3px solid #00ff88;
|
||
}
|
||
|
||
/* 高亮<think>标签内容 */
|
||
.think-content {
|
||
background: #1a2a3a;
|
||
border: 1px solid #4488ff;
|
||
padding: 10px;
|
||
margin: 10px 0;
|
||
border-radius: 4px;
|
||
color: #88bbff;
|
||
}
|
||
|
||
.status {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
padding: 10px 20px;
|
||
background: #2a2a2a;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.status.connected {
|
||
background: #00ff88;
|
||
color: #000;
|
||
}
|
||
|
||
.no-logs {
|
||
text-align: center;
|
||
color: #666;
|
||
padding: 40px;
|
||
}
|
||
|
||
.metadata {
|
||
margin-top: 10px;
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
.download-btn {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
background: #4488ff;
|
||
color: white;
|
||
padding: 10px 20px;
|
||
border-radius: 4px;
|
||
text-decoration: none;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.copy-content-btn {
|
||
float: right;
|
||
font-size: 12px;
|
||
padding: 2px 8px;
|
||
background: #3a3a3a;
|
||
border: none;
|
||
color: #888;
|
||
cursor: pointer;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.copy-content-btn:hover {
|
||
background: #00ff88;
|
||
color: #000;
|
||
}
|
||
|
||
/* 语法高亮 */
|
||
.json-key { color: #ff79c6; }
|
||
.json-string { color: #f1fa8c; }
|
||
.json-number { color: #bd93f9; }
|
||
.json-boolean { color: #50fa7b; }
|
||
.json-null { color: #ff5555; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>🔍 DeepResearch AI 调试查看器</h1>
|
||
|
||
<div class="controls">
|
||
<input type="text" id="sessionId" placeholder="输入会话ID(格式:uuid)">
|
||
<select id="logType">
|
||
<option value="all">所有日志</option>
|
||
<option value="api_calls">API调用</option>
|
||
<option value="errors">错误日志</option>
|
||
</select>
|
||
<button onclick="loadLogs()">加载日志</button>
|
||
<button onclick="connectWebSocket()">实时监听</button>
|
||
<button onclick="clearLogs()">清空显示</button>
|
||
<button onclick="toggleAllContent()">展开/折叠全部</button>
|
||
</div>
|
||
|
||
<div class="tabs">
|
||
<button class="tab active" onclick="switchTab('all')">全部</button>
|
||
<button class="tab" onclick="switchTab('r1')">R1模型</button>
|
||
<button class="tab" onclick="switchTab('v3')">V3模型</button>
|
||
<button class="tab" onclick="switchTab('errors')">错误</button>
|
||
</div>
|
||
|
||
<div class="log-container" id="logContainer">
|
||
<div class="no-logs">请输入会话ID并点击"加载日志"</div>
|
||
</div>
|
||
|
||
<div class="status" id="status">未连接</div>
|
||
|
||
<a href="#" class="download-btn" id="downloadBtn" style="display:none;" onclick="downloadLogs()">
|
||
📥 下载日志
|
||
</a>
|
||
</div>
|
||
|
||
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
||
<script>
|
||
let socket = null;
|
||
let currentSessionId = '';
|
||
let currentTab = 'all';
|
||
let allLogs = [];
|
||
let allExpanded = false;
|
||
|
||
function loadLogs() {
|
||
const sessionId = document.getElementById('sessionId').value;
|
||
const logType = document.getElementById('logType').value;
|
||
|
||
if (!sessionId) {
|
||
alert('请输入会话ID');
|
||
return;
|
||
}
|
||
|
||
currentSessionId = sessionId;
|
||
document.getElementById('downloadBtn').style.display = 'block';
|
||
|
||
fetch(`/api/research/${sessionId}/debug?type=${logType}&limit=0`)
|
||
.then(resp => resp.json())
|
||
.then(data => {
|
||
if (data.error) {
|
||
alert('加载失败: ' + data.error);
|
||
return;
|
||
}
|
||
|
||
allLogs = data.logs || [];
|
||
displayLogs();
|
||
})
|
||
.catch(err => {
|
||
alert('加载失败: ' + err.message);
|
||
});
|
||
}
|
||
|
||
function displayLogs() {
|
||
const container = document.getElementById('logContainer');
|
||
let filteredLogs = allLogs;
|
||
|
||
// 根据当前标签过滤
|
||
if (currentTab === 'r1') {
|
||
filteredLogs = allLogs.filter(log => log.agent_type === 'R1');
|
||
} else if (currentTab === 'v3') {
|
||
filteredLogs = allLogs.filter(log => log.agent_type === 'V3');
|
||
} else if (currentTab === 'errors') {
|
||
filteredLogs = allLogs.filter(log => log.type === 'json_parse_error' || log.response?.startsWith('ERROR:'));
|
||
}
|
||
|
||
if (filteredLogs.length === 0) {
|
||
container.innerHTML = '<div class="no-logs">没有找到相关日志</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = filteredLogs.map((log, index) => {
|
||
if (log.type === 'json_parse_error') {
|
||
return createErrorEntry(log, index);
|
||
} else {
|
||
return createLogEntry(log, index);
|
||
}
|
||
}).join('');
|
||
}
|
||
|
||
function createLogEntry(log, index) {
|
||
const isError = log.response?.startsWith('ERROR:');
|
||
return `
|
||
<div class="log-entry ${isError ? 'error' : ''}">
|
||
<div class="log-header">
|
||
<div>
|
||
<span class="log-model">${log.model}</span> |
|
||
<span class="log-method">${log.method}</span> |
|
||
<span>${log.agent_type}</span>
|
||
</div>
|
||
<div>${new Date(log.timestamp).toLocaleString()}</div>
|
||
</div>
|
||
<div class="log-content">
|
||
<div class="content-section">
|
||
<div class="content-label">
|
||
<span>Prompt (${log.prompt_length} 字符):</span>
|
||
<button class="copy-content-btn" onclick="copyContent('${index}_prompt')">📋 复制</button>
|
||
</div>
|
||
<div class="content-box prompt" id="${index}_prompt_content" onclick="toggleExpand(this)">
|
||
${highlightThinkTags(escapeHtml(log.prompt))}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content-section">
|
||
<div class="content-label">
|
||
<span>Response (${log.response_length} 字符):</span>
|
||
<button class="copy-content-btn" onclick="copyContent('${index}_response')">📋 复制</button>
|
||
</div>
|
||
<div class="content-box response" id="${index}_response_content" onclick="toggleExpand(this)">
|
||
${highlightContent(log.response)}
|
||
</div>
|
||
</div>
|
||
|
||
${log.metadata ? `<div class="metadata">
|
||
温度: ${log.temperature || 'N/A'} |
|
||
最大tokens: ${log.max_tokens || 'N/A'} |
|
||
Prompt tokens: ${log.metadata.prompt_tokens || 'N/A'} |
|
||
Completion tokens: ${log.metadata.completion_tokens || 'N/A'}
|
||
</div>` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function createErrorEntry(log, index) {
|
||
return `
|
||
<div class="log-entry error">
|
||
<div class="log-header">
|
||
<div>
|
||
<span>JSON解析错误</span>
|
||
</div>
|
||
<div>${new Date(log.timestamp).toLocaleString()}</div>
|
||
</div>
|
||
<div class="log-content">
|
||
<div class="content-section">
|
||
<div class="content-label">
|
||
<span>原始文本:</span>
|
||
<button class="copy-content-btn" onclick="copyContent('${index}_raw')">📋 复制</button>
|
||
</div>
|
||
<div class="content-box prompt" id="${index}_raw_content" onclick="toggleExpand(this)">
|
||
${escapeHtml(log.raw_text)}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content-section">
|
||
<div class="content-label">错误:</div>
|
||
<div class="content-box" style="border-left-color: #ff4444;">
|
||
${escapeHtml(log.error)}
|
||
</div>
|
||
</div>
|
||
|
||
${log.fixed_text ? `<div class="content-section">
|
||
<div class="content-label">
|
||
<span>修复后:</span>
|
||
<button class="copy-content-btn" onclick="copyContent('${index}_fixed')">📋 复制</button>
|
||
</div>
|
||
<div class="content-box response" id="${index}_fixed_content" onclick="toggleExpand(this)">
|
||
${escapeHtml(log.fixed_text)}
|
||
</div>
|
||
</div>` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function connectWebSocket() {
|
||
const sessionId = document.getElementById('sessionId').value;
|
||
if (!sessionId) {
|
||
alert('请先输入会话ID');
|
||
return;
|
||
}
|
||
|
||
if (socket) {
|
||
socket.disconnect();
|
||
}
|
||
|
||
// 先启用调试模式
|
||
fetch('/api/debug/enable', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({session_id: sessionId})
|
||
}).then(() => {
|
||
// 连接WebSocket
|
||
socket = io();
|
||
|
||
socket.on('connect', () => {
|
||
document.getElementById('status').textContent = '已连接';
|
||
document.getElementById('status').classList.add('connected');
|
||
|
||
// 加入会话房间
|
||
socket.emit('join_session', {session_id: sessionId});
|
||
});
|
||
|
||
socket.on('disconnect', () => {
|
||
document.getElementById('status').textContent = '已断开';
|
||
document.getElementById('status').classList.remove('connected');
|
||
});
|
||
|
||
// 监听调试日志
|
||
socket.on('ai_debug_log', (data) => {
|
||
if (data.log_entry) {
|
||
allLogs.push(data.log_entry);
|
||
displayLogs();
|
||
// 滚动到底部
|
||
const container = document.getElementById('logContainer');
|
||
container.scrollTop = container.scrollHeight;
|
||
}
|
||
});
|
||
|
||
// 监听解析错误
|
||
socket.on('parse_error', (data) => {
|
||
allLogs.push(data);
|
||
displayLogs();
|
||
});
|
||
});
|
||
}
|
||
|
||
function switchTab(tab) {
|
||
currentTab = tab;
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
event.target.classList.add('active');
|
||
displayLogs();
|
||
}
|
||
|
||
function clearLogs() {
|
||
allLogs = [];
|
||
displayLogs();
|
||
}
|
||
|
||
function downloadLogs() {
|
||
if (!currentSessionId) return;
|
||
window.location.href = `/api/research/${currentSessionId}/debug/download`;
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function highlightThinkTags(text) {
|
||
// 高亮显示<think>标签内容
|
||
return text.replace(/<think>([\s\S]*?)<\/think>/g,
|
||
'<div class="think-content"><think><br>$1<br></think></div>');
|
||
}
|
||
|
||
function highlightContent(text) {
|
||
// 先转义HTML
|
||
let escaped = escapeHtml(text);
|
||
|
||
// 高亮<think>标签
|
||
escaped = highlightThinkTags(escaped);
|
||
|
||
// 尝试高亮JSON(但不要破坏think标签的高亮)
|
||
// 只有在没有think标签的情况下才尝试JSON高亮
|
||
if (!text.includes('<think>') && (text.trim().startsWith('{') || text.trim().startsWith('['))) {
|
||
try {
|
||
// 验证是否为有效JSON
|
||
JSON.parse(text);
|
||
// 如果是,进行语法高亮
|
||
escaped = highlightJson(escaped);
|
||
} catch (e) {
|
||
// 不是有效JSON,保持原样
|
||
}
|
||
}
|
||
|
||
return escaped;
|
||
}
|
||
|
||
function highlightJson(text) {
|
||
// 简单的JSON语法高亮
|
||
text = text.replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:');
|
||
text = text.replace(/: "([^"]+)"/g, ': <span class="json-string">"$1"</span>');
|
||
text = text.replace(/: (\d+)/g, ': <span class="json-number">$1</span>');
|
||
text = text.replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>');
|
||
text = text.replace(/: null/g, ': <span class="json-null">null</span>');
|
||
return text;
|
||
}
|
||
|
||
function toggleExpand(element) {
|
||
if (element.classList.contains('collapsed')) {
|
||
element.classList.remove('collapsed');
|
||
} else {
|
||
// 只有内容超过200px高度时才折叠
|
||
if (element.scrollHeight > 200) {
|
||
element.classList.add('collapsed');
|
||
}
|
||
}
|
||
}
|
||
|
||
function toggleAllContent() {
|
||
const contentBoxes = document.querySelectorAll('.content-box');
|
||
allExpanded = !allExpanded;
|
||
|
||
contentBoxes.forEach(box => {
|
||
if (allExpanded) {
|
||
box.classList.remove('collapsed');
|
||
} else if (box.scrollHeight > 200) {
|
||
box.classList.add('collapsed');
|
||
}
|
||
});
|
||
}
|
||
|
||
function copyContent(elementId) {
|
||
const element = document.getElementById(elementId + '_content');
|
||
const text = element.textContent;
|
||
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
// 显示复制成功提示
|
||
const tooltip = document.createElement('div');
|
||
tooltip.className = 'copy-tooltip';
|
||
tooltip.textContent = '已复制!';
|
||
tooltip.style.cssText = `
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: #00ff88;
|
||
color: #000;
|
||
padding: 10px 20px;
|
||
border-radius: 8px;
|
||
z-index: 1000;
|
||
`;
|
||
document.body.appendChild(tooltip);
|
||
|
||
setTimeout(() => {
|
||
tooltip.remove();
|
||
}, 2000);
|
||
});
|
||
}
|
||
|
||
// 为复制的内容添加原始值
|
||
window.copyContent = function(prefix) {
|
||
const log = allLogs[parseInt(prefix.split('_')[0])];
|
||
let text = '';
|
||
|
||
if (prefix.endsWith('_prompt')) {
|
||
text = log.prompt;
|
||
} else if (prefix.endsWith('_response')) {
|
||
text = log.response;
|
||
} else if (prefix.endsWith('_raw')) {
|
||
text = log.raw_text;
|
||
} else if (prefix.endsWith('_fixed')) {
|
||
text = log.fixed_text;
|
||
}
|
||
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
const tooltip = document.createElement('div');
|
||
tooltip.className = 'copy-tooltip';
|
||
tooltip.textContent = '已复制原始内容!';
|
||
tooltip.style.cssText = `
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: #00ff88;
|
||
color: #000;
|
||
padding: 10px 20px;
|
||
border-radius: 8px;
|
||
z-index: 1000;
|
||
`;
|
||
document.body.appendChild(tooltip);
|
||
|
||
setTimeout(() => {
|
||
tooltip.remove();
|
||
}, 2000);
|
||
});
|
||
};
|
||
|
||
// 自动加载URL中的session ID
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const sessionIdParam = urlParams.get('session_id');
|
||
if (sessionIdParam) {
|
||
document.getElementById('sessionId').value = sessionIdParam;
|
||
loadLogs();
|
||
connectWebSocket();
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |