deepresearch/app/templates/debug.html
2025-07-02 15:35:36 +08:00

669 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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(/&lt;think&gt;([\s\S]*?)&lt;\/think&gt;/g,
'<div class="think-content">&lt;think&gt;<br>$1<br>&lt;/think&gt;</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>