first commit

This commit is contained in:
闫旭隆
2025-10-17 09:31:28 +08:00
commit 4698145045
589 changed files with 196795 additions and 0 deletions

806
AIEC-RAG/test_stream.html Normal file
View File

@ -0,0 +1,806 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RAG流式接口测试</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.2em;
opacity: 0.9;
}
.test-panel {
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.input-section {
margin-bottom: 30px;
}
.input-group {
display: flex;
gap: 15px;
margin-bottom: 15px;
}
input[type="text"] {
flex: 1;
padding: 15px 20px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-size: 16px;
transition: all 0.3s;
}
input[type="text"]:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.button-group {
display: flex;
gap: 10px;
}
button {
padding: 15px 30px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.btn-primary:disabled {
background: #cbd5e0;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #f7fafc;
color: #4a5568;
border: 2px solid #e2e8f0;
}
.btn-secondary:hover {
background: #edf2f7;
}
.status-section {
margin-bottom: 30px;
padding: 20px;
background: #f7fafc;
border-radius: 10px;
min-height: 400px;
}
.status-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #e2e8f0;
}
.status-title {
font-size: 1.3em;
font-weight: 600;
color: #2d3748;
}
.status-timer {
font-size: 1.1em;
color: #718096;
font-weight: 500;
}
.progress-bar {
height: 8px;
background: #e2e8f0;
border-radius: 10px;
overflow: hidden;
margin-bottom: 20px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
transition: width 0.3s ease;
position: relative;
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.3),
transparent
);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.status-content {
max-height: 300px;
overflow-y: auto;
padding: 10px;
}
.status-item {
padding: 12px 15px;
margin-bottom: 10px;
background: white;
border-radius: 8px;
border-left: 4px solid #667eea;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.status-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.status-type {
font-weight: 600;
color: #4a5568;
}
.status-time {
font-size: 0.9em;
color: #a0aec0;
}
.status-message {
color: #2d3748;
line-height: 1.5;
}
.status-data {
margin-top: 10px;
padding: 10px;
background: #f7fafc;
border-radius: 6px;
font-size: 0.95em;
}
.sub-queries {
margin-top: 8px;
padding-left: 20px;
}
.sub-queries li {
color: #4a5568;
margin-bottom: 5px;
}
.answer-section {
padding: 20px;
background: #f0fdf4;
border-radius: 10px;
border: 2px solid #86efac;
display: none;
}
.answer-section.show {
display: block;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.answer-header {
font-size: 1.2em;
font-weight: 600;
color: #166534;
margin-bottom: 15px;
}
.answer-content {
color: #2d3748;
line-height: 1.8;
white-space: pre-wrap;
}
.test-options {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 15px;
background: #f7fafc;
border-radius: 8px;
}
.option-group {
display: flex;
align-items: center;
gap: 8px;
}
.option-group label {
font-weight: 500;
color: #4a5568;
}
select {
padding: 8px 12px;
border: 2px solid #e2e8f0;
border-radius: 6px;
background: white;
cursor: pointer;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.stat-card {
padding: 15px;
background: white;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 1.8em;
font-weight: bold;
color: #667eea;
}
.stat-label {
font-size: 0.9em;
color: #718096;
margin-top: 5px;
}
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error-message {
padding: 15px;
background: #fee;
border: 1px solid #fcc;
border-radius: 8px;
color: #c00;
margin-bottom: 20px;
display: none;
}
.error-message.show {
display: block;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 RAG流式接口测试</h1>
<p>测试Server-Sent Events (SSE)实时状态推送</p>
</div>
<div class="test-panel">
<!-- 测试选项 -->
<div class="test-options">
<div class="option-group">
<label>API地址:</label>
<input type="text" id="apiUrl" value="http://localhost:8080" style="width: 200px;">
</div>
<div class="option-group">
<label>模式:</label>
<select id="mode">
<option value="0">自动判断</option>
<option value="simple">强制简单</option>
<option value="complex">强制复杂</option>
</select>
</div>
</div>
<!-- 输入区域 -->
<div class="input-section">
<div class="input-group">
<input type="text" id="queryInput" placeholder="输入您的问题例如什么是RAG系统" value="什么是RAG系统">
</div>
<div class="button-group">
<button id="streamBtn" class="btn-primary" onclick="testStream()">
<span id="streamBtnText">🔄 测试流式接口</span>
<span id="streamSpinner" class="spinner" style="display: none;"></span>
</button>
<button id="normalBtn" class="btn-secondary" onclick="testNormal()">
📋 测试普通接口
</button>
<button id="stopBtn" class="btn-secondary" onclick="stopStream()" style="display: none;">
⏹️ 停止
</button>
<button class="btn-secondary" onclick="clearStatus()">
🗑️ 清空
</button>
</div>
</div>
<!-- 错误提示 -->
<div id="errorMessage" class="error-message"></div>
<!-- 状态显示区域 -->
<div class="status-section">
<div class="status-header">
<div class="status-title" id="statusTitle">等待测试...</div>
<div class="status-timer" id="statusTimer">⏱️ 0.0秒</div>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressBar" style="width: 0%"></div>
</div>
<div class="status-content" id="statusContent">
<!-- 状态项将动态添加到这里 -->
</div>
<!-- 统计信息 -->
<div class="stats" id="stats" style="display: none;">
<div class="stat-card">
<div class="stat-value" id="statEvents">0</div>
<div class="stat-label">事件数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statDocs">0</div>
<div class="stat-label">文档数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statIterations">0</div>
<div class="stat-label">迭代轮次</div>
</div>
<div class="stat-card">
<div class="stat-value" id="statTime">0</div>
<div class="stat-label">总耗时(秒)</div>
</div>
</div>
</div>
<!-- 答案显示区域 -->
<div id="answerSection" class="answer-section">
<div class="answer-header">💡 最终答案</div>
<div class="answer-content" id="answerContent"></div>
</div>
</div>
</div>
<script>
let eventSource = null;
let startTime = null;
let timerInterval = null;
let eventCount = 0;
let totalDocs = 0;
let iterations = 0;
function testStream() {
const query = document.getElementById('queryInput').value;
const apiUrl = document.getElementById('apiUrl').value;
const mode = document.getElementById('mode').value;
if (!query.trim()) {
showError('请输入查询问题');
return;
}
// 重置状态
clearStatus();
hideError();
// 更新UI
document.getElementById('streamBtn').disabled = true;
document.getElementById('streamBtnText').textContent = '连接中...';
document.getElementById('streamSpinner').style.display = 'inline-block';
document.getElementById('stopBtn').style.display = 'inline-block';
document.getElementById('statusTitle').textContent = '正在连接服务器...';
document.getElementById('stats').style.display = 'grid';
// 开始计时
startTime = Date.now();
timerInterval = setInterval(updateTimer, 100);
// 构建查询参数
const params = new URLSearchParams({
query: query,
mode: mode,
save_output: 'false'
});
// 创建EventSource连接
const url = `${apiUrl}/retrieve/stream`;
// 使用POST请求的EventSource需要polyfill或改用fetch
// 由于标准EventSource不支持POST我们使用fetch + ReadableStream
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
},
body: JSON.stringify({
query: query,
mode: mode,
save_output: false
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
function processText(text) {
buffer += text;
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
onStreamComplete();
return false; // 停止读取
}
try {
const event = JSON.parse(data);
handleEvent(event);
} catch (e) {
console.error('解析错误:', e, data);
}
}
}
return true; // 继续读取
}
function read() {
reader.read().then(({done, value}) => {
if (done) {
onStreamComplete();
return;
}
const text = decoder.decode(value, {stream: true});
const shouldContinue = processText(text);
if (shouldContinue) {
read();
}
}).catch(error => {
console.error('读取错误:', error);
showError('流式读取失败: ' + error.message);
onStreamComplete();
});
}
// 开始读取流
document.getElementById('statusTitle').textContent = '接收数据流...';
document.getElementById('streamBtnText').textContent = '接收中...';
read();
})
.catch(error => {
console.error('连接错误:', error);
showError('连接失败: ' + error.message);
onStreamComplete();
});
}
function handleEvent(event) {
eventCount++;
const progress = event.progress || 0;
document.getElementById('progressBar').style.width = progress + '%';
// 创建状态项
const statusItem = document.createElement('div');
statusItem.className = 'status-item';
const header = document.createElement('div');
header.className = 'status-item-header';
const typeLabel = document.createElement('span');
typeLabel.className = 'status-type';
typeLabel.textContent = getTypeLabel(event.type);
const timeLabel = document.createElement('span');
timeLabel.className = 'status-time';
timeLabel.textContent = new Date().toLocaleTimeString();
header.appendChild(typeLabel);
header.appendChild(timeLabel);
statusItem.appendChild(header);
// 根据类型处理不同的事件
if (event.message) {
const message = document.createElement('div');
message.className = 'status-message';
message.textContent = event.message;
statusItem.appendChild(message);
}
// 处理特定类型的数据
if (event.type === 'sub_queries' && event.formatted_data) {
const list = document.createElement('ol');
list.className = 'sub-queries';
event.formatted_data.forEach(q => {
const li = document.createElement('li');
li.textContent = q;
list.appendChild(li);
});
statusItem.appendChild(list);
} else if (event.type === 'documents' && event.data) {
totalDocs = event.data.count || totalDocs;
document.getElementById('statDocs').textContent = totalDocs;
if (event.formatted_sources) {
const sources = document.createElement('div');
sources.className = 'status-data';
sources.textContent = '来源: ' + event.formatted_sources.join(', ');
statusItem.appendChild(sources);
}
} else if (event.type === 'iteration' && event.data) {
iterations = event.data.current || iterations;
document.getElementById('statIterations').textContent =
`${event.data.current}/${event.data.max}`;
} else if (event.type === 'answer' && event.data) {
displayAnswer(event.data.content);
document.getElementById('statDocs').textContent =
event.data.total_documents || totalDocs;
}
// 添加到状态内容
document.getElementById('statusContent').appendChild(statusItem);
// 更新事件计数
document.getElementById('statEvents').textContent = eventCount;
// 滚动到底部
const container = document.getElementById('statusContent');
container.scrollTop = container.scrollHeight;
}
function getTypeLabel(type) {
const labels = {
'starting': '🚀 开始',
'complexity_check': '🤔 复杂度分析',
'sub_queries': '📝 子查询',
'documents': '📚 文档检索',
'iteration': '🔄 迭代',
'sufficiency_check': '✅ 充分性检查',
'parallel_retrieval': '⚡ 并行检索',
'generating': '✍️ 生成答案',
'answer': '💡 答案',
'error': '❌ 错误'
};
return labels[type] || type;
}
function displayAnswer(content) {
document.getElementById('answerContent').textContent = content;
document.getElementById('answerSection').classList.add('show');
}
function onStreamComplete() {
clearInterval(timerInterval);
document.getElementById('streamBtn').disabled = false;
document.getElementById('streamBtnText').textContent = '🔄 测试流式接口';
document.getElementById('streamSpinner').style.display = 'none';
document.getElementById('stopBtn').style.display = 'none';
document.getElementById('statusTitle').textContent = '✅ 完成';
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
document.getElementById('statTime').textContent = elapsed;
}
function stopStream() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
onStreamComplete();
document.getElementById('statusTitle').textContent = '⏹️ 已停止';
}
function clearStatus() {
document.getElementById('statusContent').innerHTML = '';
document.getElementById('answerSection').classList.remove('show');
document.getElementById('progressBar').style.width = '0%';
document.getElementById('statusTitle').textContent = '等待测试...';
document.getElementById('statusTimer').textContent = '⏱️ 0.0秒';
document.getElementById('stats').style.display = 'none';
eventCount = 0;
totalDocs = 0;
iterations = 0;
hideError();
}
function updateTimer() {
if (startTime) {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
document.getElementById('statusTimer').textContent = `⏱️ ${elapsed}`;
}
}
function testNormal() {
const query = document.getElementById('queryInput').value;
const apiUrl = document.getElementById('apiUrl').value;
const mode = document.getElementById('mode').value;
if (!query.trim()) {
showError('请输入查询问题');
return;
}
clearStatus();
hideError();
document.getElementById('normalBtn').disabled = true;
document.getElementById('statusTitle').textContent = '调用普通接口...';
startTime = Date.now();
timerInterval = setInterval(updateTimer, 100);
fetch(`${apiUrl}/retrieve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: query,
mode: mode,
save_output: false
})
})
.then(response => response.json())
.then(data => {
clearInterval(timerInterval);
document.getElementById('normalBtn').disabled = false;
document.getElementById('statusTitle').textContent = '✅ 普通接口响应';
if (data.answer) {
displayAnswer(data.answer);
}
// 显示响应信息
const statusItem = document.createElement('div');
statusItem.className = 'status-item';
statusItem.innerHTML = `
<div class="status-message">
成功获取响应<br>
答案长度: ${data.answer ? data.answer.length : 0} 字符<br>
文档数: ${data.total_passages || 0}<br>
耗时: ${((Date.now() - startTime) / 1000).toFixed(1)}
</div>
`;
document.getElementById('statusContent').appendChild(statusItem);
})
.catch(error => {
clearInterval(timerInterval);
document.getElementById('normalBtn').disabled = false;
showError('请求失败: ' + error.message);
});
}
function showError(message) {
const errorEl = document.getElementById('errorMessage');
errorEl.textContent = message;
errorEl.classList.add('show');
}
function hideError() {
document.getElementById('errorMessage').classList.remove('show');
}
// 页面加载时的初始化
window.onload = function() {
// 检查服务状态
const apiUrl = document.getElementById('apiUrl').value;
fetch(`${apiUrl}/health`)
.then(response => response.json())
.then(data => {
console.log('服务状态:', data);
})
.catch(error => {
showError('⚠️ 无法连接到服务,请确保后端服务正在运行 (端口8080)');
});
};
</script>
</body>
</html>