pd 4 days ago
parent
commit
5302428b32
  1. 948
      deepsearcher/backend/templates/index.html
  2. 88
      deepsearcher/templates/html/index.html
  3. 88
      deepsearcher/templates/index.html
  4. 280
      deepsearcher/templates/static/css/styles.css
  5. 446
      deepsearcher/templates/static/js/app.js
  6. 8
      main.py
  7. 38
      test_fix.py

948
deepsearcher/backend/templates/index.html

@ -1,948 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>DeepSearcher - 智能搜索系统</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.1.0/github-markdown-light.css"
/>
<style>
:root {
--primary-color: #4f46e5;
--secondary-color: #f9fafb;
--border-color: #e5e7eb;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--success-color: #10b981;
--error-color: #ef4444;
--warning-color: #f59e0b;
--info-color: #3b82f6;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: var(--text-primary);
background-color: #f3f4f6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
h1 {
color: var(--primary-color);
margin-bottom: 10px;
}
.app-description {
color: var(--text-secondary);
max-width: 800px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 24px;
margin-bottom: 24px;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 16px;
color: var(--text-primary);
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
input,
textarea,
select {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 16px;
transition: border-color 0.15s;
}
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
}
button {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 6px;
padding: 12px 20px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s;
}
button:hover {
background-color: #4338ca;
}
button:disabled {
background-color: var(--border-color);
cursor: not-allowed;
}
.btn-secondary {
background-color: var(--text-secondary);
}
.btn-secondary:hover {
background-color: var(--text-primary);
}
.result-container {
margin-top: 20px;
padding: 16px;
border-radius: 6px;
background-color: var(--secondary-color);
display: none;
}
.result-container.visible {
display: block;
}
.status {
padding: 12px;
border-radius: 6px;
margin: 10px 0;
display: none;
}
.status.visible {
display: block;
}
.status-success {
background-color: #d1fae5;
color: var(--success-color);
border: 1px solid var(--success-color);
}
.status-error {
background-color: #fee2e2;
color: var(--error-color);
border: 1px solid var(--error-color);
}
.status-loading {
background-color: #fffbeb;
color: var(--warning-color);
border: 1px solid var(--warning-color);
display: flex;
align-items: center;
}
.loading-spinner {
display: none;
width: 12px;
height: 12px;
border: 2px solid #f3f3f3;
border-top: 2px solid var(--warning-color);
border-radius: 50%;
animation: spin 1s linear infinite;
flex-shrink: 0;
align-items: center;
}
.status-loading .loading-spinner {
display: inline-block;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading .loading-spinner {
display: inline-block;
}
.query-result {
white-space: pre-wrap;
line-height: 1.6;
background-color: white;
padding: 16px;
border-radius: 6px;
border: 1px solid var(--border-color);
margin-top: 8px;
}
.message-stream {
margin-top: 16px;
}
#processResult {
margin-top: 16px;
}
#processResult h3 {
color: var(--text-secondary);
font-size: 1rem;
}
#queryResult h3 {
color: var(--text-secondary);
font-size: 1rem;
}
.message-container {
border: 1px solid var(--border-color);
border-radius: 6px;
background-color: white;
padding: 12px;
}
.message {
margin-bottom: 12px;
padding: 8px 12px;
border-radius: 4px;
border-left: 4px solid;
font-size: 14px;
line-height: 1.4;
}
.message-start {
background-color: #f0f9ff;
border-left-color: var(--info-color);
}
.message-info {
background-color: #fef3c7;
border-left-color: var(--warning-color);
}
.message-answer {
background-color: #d1fae5;
border-left-color: var(--success-color);
}
.message-complete {
background-color: #dbeafe;
border-left-color: var(--primary-color);
}
.message-error {
background-color: #fee2e2;
border-left-color: var(--error-color);
}
.message-timestamp {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
}
footer {
text-align: center;
margin-top: 30px;
padding: 20px;
color: var(--text-secondary);
font-size: 0.875rem;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.card {
padding: 16px;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>DeepSearcher 智能搜索系统</h1>
<p class="app-description">
基于大型语言模型和向量数据库的企业知识管理系统,支持私有数据搜索和在线内容整合,提供准确答案和综合报告。
</p>
</header>
<main>
<div class="card">
<h2 class="card-title">文件加载</h2>
<div class="form-group">
<label for="filePaths">文件路径(多个路径用逗号分隔)</label>
<input
type="text"
id="filePaths"
placeholder="例如: /path/to/file1.pdf,/path/to/file2.txt"
/>
</div>
<div class="form-group">
<label for="collectionName">集合名称(可选)</label>
<input
type="text"
id="collectionName"
placeholder="例如: my_collection"
/>
</div>
<div class="form-group">
<label for="collectionDesc">集合描述(可选)</label>
<textarea
id="collectionDesc"
rows="2"
placeholder="例如: 这是一个测试集合"
></textarea>
</div>
<button id="loadFilesBtn">加载文件</button>
<div
id="loadStatus"
class="status"
></div>
</div>
<div class="card">
<h2 class="card-title">网站内容加载</h2>
<div class="form-group">
<label for="websiteUrls">网站URL(多个URL用逗号分隔)</label>
<input
type="text"
id="websiteUrls"
placeholder="例如: https://example.com/page1,https://example.com/page2"
/>
</div>
<div class="form-group">
<label for="webCollectionName">集合名称(可选)</label>
<input
type="text"
id="webCollectionName"
placeholder="例如: web_collection"
/>
</div>
<div class="form-group">
<label for="webCollectionDesc">集合描述(可选)</label>
<textarea
id="webCollectionDesc"
rows="2"
placeholder="例如: 来自网站的内容"
></textarea>
</div>
<button id="loadWebsiteBtn">加载网站内容</button>
<div
id="webLoadStatus"
class="status"
></div>
</div>
<div class="card">
<h2 class="card-title">智能查询</h2>
<div class="form-group">
<label for="queryText">请输入您的问题</label>
<textarea
id="queryText"
rows="3"
placeholder="例如: 请生成一份关于人工智能发展趋势的报告"
></textarea>
</div>
<div class="form-group">
<label for="maxIter">最大迭代次数 (1-10)</label>
<input
type="number"
id="maxIter"
min="1"
max="10"
value="3"
/>
</div>
<button id="queryBtn">执行查询</button>
<button
id="clearMessagesBtn"
style="margin-left: 10px; background-color: var(--text-secondary)"
>
清空消息
</button>
<div
id="queryStatus"
class="status"
></div>
<div
id="queryResult"
class="result-container"
>
<h3>查询结果:</h3>
<div
class="query-result"
id="resultText"
></div>
</div>
<div
id="processResult"
class="result-container"
>
<h3>处理过程:</h3>
<div
id="messageStream"
class="message-stream"
>
<div
class="message-container"
id="messageContainer"
></div>
</div>
</div>
</div>
</main>
<footer>
<p>DeepSearcher © 2025 | 企业知识管理与智能问答系统</p>
</footer>
</div>
<script>
// 全局变量
let eventSource = null;
let isStreaming = false;
// 工具函数:显示状态信息
function showStatus(elementId, message, type) {
const statusElement = document.getElementById(elementId);
// 清除之前的类型类
statusElement.classList.remove(
'status-success',
'status-error',
'status-loading'
);
// 添加新的类型类
if (type === 'success') {
statusElement.classList.add('status-success');
statusElement.innerHTML = message;
} else if (type === 'error') {
statusElement.classList.add('status-error');
statusElement.innerHTML = message;
} else if (type === 'loading') {
statusElement.classList.add('status-loading');
statusElement.innerHTML = `<div class="loading-spinner"></div>${message}`;
}
statusElement.classList.add('visible');
}
// 工具函数:显示消息流
function displayMessages(messages) {
const container = document.getElementById('messageContainer');
container.innerHTML = '';
messages.forEach((message) => {
addMessageToContainer(message);
});
// 滚动到底部
container.scrollTop = container.scrollHeight;
}
// 工具函数:添加单个消息到容器
function addMessageToContainer(message) {
console.log('Adding message to container:', message);
const container = document.getElementById('messageContainer');
if (!container) {
console.error('Message container not found!');
return;
}
const messageElement = document.createElement('div');
messageElement.className = `message message-${message.type}`;
const contentElement = document.createElement('div');
contentElement.textContent = message.content;
messageElement.appendChild(contentElement);
// 只有在有有效时间戳时才显示时间
if (message.timestamp && !isNaN(message.timestamp)) {
const date = new Date(message.timestamp * 1000);
if (!isNaN(date.getTime())) {
const timestampElement = document.createElement('div');
timestampElement.className = 'message-timestamp';
timestampElement.textContent = date.toLocaleTimeString();
messageElement.appendChild(timestampElement);
}
}
container.appendChild(messageElement);
// 确保处理过程容器是可见的
const processContainer = document.getElementById('processResult');
if (
processContainer &&
!processContainer.classList.contains('visible')
) {
processContainer.classList.add('visible');
}
// 滚动到底部
container.scrollTop = container.scrollHeight;
console.log(
'Message added successfully, container now has',
container.children.length,
'messages'
);
}
// 工具函数:隐藏状态信息
function hideStatus(elementId) {
const statusElement = document.getElementById(elementId);
statusElement.classList.remove('visible');
}
// 工具函数:显示结果
function showResult() {
const resultElement = document.getElementById('queryResult');
resultElement.classList.add('visible');
}
// 工具函数:隐藏结果
function hideResult() {
const resultElement = document.getElementById('queryResult');
resultElement.classList.remove('visible');
}
// 工具函数:显示处理过程
function showProcessResult() {
const processElement = document.getElementById('processResult');
processElement.classList.add('visible');
}
// 工具函数:隐藏处理过程
function hideProcessResult() {
const processElement = document.getElementById('processResult');
processElement.classList.remove('visible');
}
// 工具函数:转义HTML特殊字符
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function (m) {
return map[m];
});
}
// 工具函数:设置按钮加载状态
function setButtonLoading(button, loading) {
if (loading) {
button.classList.add('loading');
button.disabled = true;
} else {
button.classList.remove('loading');
button.disabled = false;
}
}
// 工具函数:关闭EventSource连接
function closeEventSource() {
if (eventSource) {
console.log('Closing eventSource in closeEventSource function');
eventSource.close();
eventSource = null;
}
if (window.currentEventSource) {
console.log(
'Closing currentEventSource in closeEventSource function'
);
window.currentEventSource.close();
window.currentEventSource = null;
}
isStreaming = false;
}
// 工具函数:处理实时消息流
function handleStreamMessage(data) {
try {
const message = JSON.parse(data);
switch (message.type) {
case 'connection':
console.log('Connected to message stream:', message.message);
break;
case 'heartbeat':
// 心跳消息,不需要处理
break;
case 'start':
console.log('Query started:', message.content);
showStatus('queryStatus', ' 正在处理...', 'loading');
addMessageToContainer(message);
break;
case 'complete':
console.log('Query completed - closing connection');
showStatus('queryStatus', '查询完成', 'success');
addMessageToContainer(message);
// 关闭EventSource连接
if (window.currentEventSource) {
console.log('Closing currentEventSource');
window.currentEventSource.close();
window.currentEventSource = null;
}
isStreaming = false;
setButtonLoading(document.getElementById('queryBtn'), false);
console.log(
'Query completed - connection closed, isStreaming set to false'
);
break;
case 'error':
console.error('Error:', message.content);
showStatus('queryStatus', message.content, 'error');
addMessageToContainer(message);
// 关闭EventSource连接
if (window.currentEventSource) {
window.currentEventSource.close();
window.currentEventSource = null;
}
isStreaming = false;
setButtonLoading(document.getElementById('queryBtn'), false);
break;
case 'info':
// 处理信息消息
console.log(
'Processing info message:',
message.content.substring(0, 100) + '...'
);
addMessageToContainer(message);
break;
case 'answer':
// 处理answer类型,显示查询结果
console.log(
'Processing answer message:',
message.content.substring(0, 100) + '...'
);
// 将结果内容显示在结果区域
if (
message.content &&
message.content !== '==== FINAL ANSWER===='
) {
// document.getElementById('resultText').textContent =
// marked.parse(message.content);
document.getElementById('resultText').innerHTML = marked.parse(
message.content
);
showResult();
}
// 不将answer消息添加到处理过程容器中,只显示在查询结果框中
break;
default:
console.log('Unknown message type:', message.type);
}
} catch (error) {
console.error('Error parsing message:', error);
}
}
// 工具函数:开始实时消息流
function startMessageStream() {
closeEventSource(); // 关闭之前的连接
eventSource = new EventSource('/stream-messages/');
eventSource.onopen = function (event) {
console.log('EventSource connection opened');
};
eventSource.onmessage = function (event) {
handleStreamMessage(event.data);
};
eventSource.onerror = function (event) {
console.error('EventSource error:', event);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('EventSource connection closed');
}
};
}
// 加载文件功能
document
.getElementById('loadFilesBtn')
.addEventListener('click', async function () {
const button = this;
const filePathsInput = document.getElementById('filePaths').value;
const collectionName =
document.getElementById('collectionName').value;
const collectionDesc =
document.getElementById('collectionDesc').value;
if (!filePathsInput) {
showStatus('loadStatus', '请提供至少一个文件路径', 'error');
return;
}
const filePaths = filePathsInput
.split(',')
.map((path) => path.trim())
.filter((path) => path);
setButtonLoading(button, true);
showStatus('loadStatus', '正在加载文件...', 'loading');
hideResult();
hideProcessResult();
try {
const response = await fetch('/load-files/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
paths: filePaths,
collection_name: collectionName || undefined,
collection_description: collectionDesc || undefined
})
});
const data = await response.json();
if (response.ok) {
showStatus('loadStatus', data.message, 'success');
} else {
showStatus('loadStatus', `加载失败: ${data.detail}`, 'error');
}
} catch (error) {
showStatus('loadStatus', `请求失败: ${error.message}`, 'error');
} finally {
setButtonLoading(button, false);
}
});
// 清空消息功能
document
.getElementById('clearMessagesBtn')
.addEventListener('click', async function () {
try {
const response = await fetch('/clear-messages/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
// 清空消息容器
const container = document.getElementById('messageContainer');
container.innerHTML = '';
// 清空查询结果
const resultText = document.getElementById('resultText');
resultText.textContent = '';
// 隐藏处理过程容器
hideProcessResult();
// 隐藏查询结果容器
hideResult();
showStatus('queryStatus', '消息已清空', 'success');
} else {
showStatus('queryStatus', '清空消息失败', 'error');
}
} catch (error) {
showStatus('queryStatus', `请求失败: ${error.message}`, 'error');
}
});
// 加载网站内容功能
document
.getElementById('loadWebsiteBtn')
.addEventListener('click', async function () {
const button = this;
const urlsInput = document.getElementById('websiteUrls').value;
const collectionName =
document.getElementById('webCollectionName').value;
const collectionDesc =
document.getElementById('webCollectionDesc').value;
if (!urlsInput) {
showStatus('webLoadStatus', '请提供至少一个网站URL', 'error');
return;
}
const urls = urlsInput
.split(',')
.map((url) => url.trim())
.filter((url) => url);
setButtonLoading(button, true);
showStatus('webLoadStatus', '正在加载网站内容...', 'loading');
hideResult();
hideProcessResult();
try {
const response = await fetch('/load-website/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
urls: urls,
collection_name: collectionName || undefined,
collection_description: collectionDesc || undefined
})
});
const data = await response.json();
if (response.ok) {
showStatus('webLoadStatus', data.message, 'success');
} else {
showStatus('webLoadStatus', `加载失败: ${data.detail}`, 'error');
}
} catch (error) {
showStatus('webLoadStatus', `请求失败: ${error.message}`, 'error');
} finally {
setButtonLoading(button, false);
}
});
// 查询功能 - 使用实时流
document
.getElementById('queryBtn')
.addEventListener('click', async function () {
const button = this;
const queryText = document.getElementById('queryText').value;
const maxIter = parseInt(document.getElementById('maxIter').value);
if (!queryText) {
showStatus('queryStatus', '请输入查询问题', 'error');
return;
}
if (isNaN(maxIter) || maxIter < 1 || maxIter > 10) {
showStatus('queryStatus', '迭代次数必须是1到10之间的整数', 'error');
return;
}
if (isStreaming) {
console.log('Query already in progress, isStreaming:', isStreaming);
showStatus('queryStatus', '查询正在进行中,请等待完成', 'error');
return;
}
setButtonLoading(button, true);
showStatus('queryStatus', '正在启动查询...', 'loading');
hideResult();
hideProcessResult();
// 清空消息容器
const container = document.getElementById('messageContainer');
container.innerHTML = '';
try {
console.log('Starting new query, setting isStreaming to true');
isStreaming = true;
// 确保没有其他连接存在
if (window.currentEventSource) {
console.log('Closing existing EventSource connection');
window.currentEventSource.close();
window.currentEventSource = null;
}
// 使用EventSource直接连接到查询流
const eventSource = new EventSource(
`/query-stream/?original_query=${encodeURIComponent(
queryText
)}&max_iter=${maxIter}`
);
// 保存EventSource引用以便后续关闭
window.currentEventSource = eventSource;
eventSource.onopen = function (event) {
console.log('EventSource connection opened for query');
showStatus('queryStatus', ' 正在处理...', 'loading');
};
eventSource.onmessage = function (event) {
console.log('Received message:', event.data);
handleStreamMessage(event.data);
};
eventSource.onerror = function (event) {
console.error('EventSource error:', event);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('EventSource connection closed due to error');
isStreaming = false;
setButtonLoading(button, false);
window.currentEventSource = null;
}
};
} catch (error) {
console.error('Query error:', error);
showStatus('queryStatus', `请求失败: ${error.message}`, 'error');
isStreaming = false;
setButtonLoading(button, false);
}
});
// 页面卸载时清理连接
window.addEventListener('beforeunload', function () {
if (window.currentEventSource) {
window.currentEventSource.close();
window.currentEventSource = null;
}
});
</script>
</body>
</html>

88
deepsearcher/templates/html/index.html

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeepSearcher - 智能搜索系统</title>
<link rel="stylesheet" href="../static/css/styles.css">
</head>
<body>
<div class="container">
<header>
<h1>DeepSearcher 智能搜索系统</h1>
<p class="app-description">基于大型语言模型和向量数据库的企业知识管理系统,支持私有数据搜索和在线内容整合,提供准确答案和综合报告。</p>
</header>
<main>
<div class="card">
<h2 class="card-title">文件加载</h2>
<div class="form-group">
<label for="filePaths">文件路径(多个路径用逗号分隔)</label>
<input type="text" id="filePaths" placeholder="例如: /path/to/file1.pdf,/path/to/file2.txt">
</div>
<div class="form-group">
<label for="collectionName">集合名称(可选)</label>
<input type="text" id="collectionName" placeholder="例如: my_collection">
</div>
<div class="form-group">
<label for="collectionDesc">集合描述(可选)</label>
<textarea id="collectionDesc" rows="2" placeholder="例如: 这是一个测试集合"></textarea>
</div>
<button id="loadFilesBtn">加载文件</button>
<div id="loadStatus" class="status"></div>
</div>
<div class="card">
<h2 class="card-title">网站内容加载</h2>
<div class="form-group">
<label for="websiteUrls">网站URL(多个URL用逗号分隔)</label>
<input type="text" id="websiteUrls" placeholder="例如: https://example.com/page1,https://example.com/page2">
</div>
<div class="form-group">
<label for="webCollectionName">集合名称(可选)</label>
<input type="text" id="webCollectionName" placeholder="例如: web_collection">
</div>
<div class="form-group">
<label for="webCollectionDesc">集合描述(可选)</label>
<textarea id="webCollectionDesc" rows="2" placeholder="例如: 来自网站的内容"></textarea>
</div>
<button id="loadWebsiteBtn">加载网站内容</button>
<div id="webLoadStatus" class="status"></div>
</div>
<div class="card">
<h2 class="card-title">智能查询</h2>
<div class="form-group">
<label for="queryText">请输入您的问题</label>
<textarea id="queryText" rows="3" placeholder="例如: 请生成一份关于人工智能发展趋势的报告"></textarea>
</div>
<div class="form-group">
<label for="maxIter">最大迭代次数 (1-10)</label>
<input type="number" id="maxIter" min="1" max="10" value="3">
</div>
<button id="queryBtn">执行查询</button>
<button id="clearMessagesBtn" style="margin-left: 10px; background-color: var(--text-secondary);">清空消息</button>
<div id="queryStatus" class="status"></div>
<div id="queryResult" class="result-container">
<h3>查询结果:</h3>
<div class="query-result" id="resultText"></div>
</div>
<div id="processResult" class="result-container">
<h3>处理过程:</h3>
<div id="messageStream" class="message-stream">
<div class="message-container" id="messageContainer"></div>
</div>
</div>
</div>
</main>
<footer>
<p>DeepSearcher © 2025 | 企业知识管理与智能问答系统</p>
</footer>
</div>
<script src="../static/js/app.js"></script>
</body>
</html>

88
deepsearcher/templates/index.html

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeepSearcher - 智能搜索系统</title>
<link rel="stylesheet" href="static/css/styles.css">
</head>
<body>
<div class="container">
<header>
<h1>DeepSearcher 智能搜索系统</h1>
<p class="app-description">基于大型语言模型和向量数据库的企业知识管理系统,支持私有数据搜索和在线内容整合,提供准确答案和综合报告。</p>
</header>
<main>
<div class="card">
<h2 class="card-title">文件加载</h2>
<div class="form-group">
<label for="filePaths">文件路径(多个路径用逗号分隔)</label>
<input type="text" id="filePaths" placeholder="例如: /path/to/file1.pdf,/path/to/file2.txt">
</div>
<div class="form-group">
<label for="collectionName">集合名称</label>
<input type="text" id="collectionName" placeholder="例如: my_collection">
</div>
<div class="form-group">
<label for="collectionDesc">集合描述</label>
<textarea id="collectionDesc" rows="2" placeholder="例如: 这是一个测试集合"></textarea>
</div>
<button id="loadFilesBtn">加载文件</button>
<div id="loadStatus" class="status"></div>
</div>
<div class="card">
<h2 class="card-title">网站加载</h2>
<div class="form-group">
<label for="websiteUrls">网站URL(多个URL用逗号分隔)</label>
<input type="text" id="websiteUrls" placeholder="例如: https://example.com/page1,https://example.com/page2">
</div>
<div class="form-group">
<label for="webCollectionName">集合名称</label>
<input type="text" id="webCollectionName" placeholder="例如: web_collection">
</div>
<div class="form-group">
<label for="webCollectionDesc">集合描述</label>
<textarea id="webCollectionDesc" rows="2" placeholder="例如: 来自网站的内容"></textarea>
</div>
<button id="loadWebsiteBtn">加载网站</button>
<div id="webLoadStatus" class="status"></div>
</div>
<div class="card">
<h2 class="card-title">智能查询</h2>
<div class="form-group">
<label for="queryText">请输入您的问题</label>
<textarea id="queryText" rows="3" placeholder="例如: 请生成一份关于人工智能发展趋势的报告"></textarea>
</div>
<div class="form-group">
<label for="maxIter">最大迭代次数 (1-10)</label>
<input type="number" id="maxIter" min="1" max="10" value="3">
</div>
<button id="queryBtn">执行查询</button>
<button id="clearMessagesBtn" style="margin-left: 10px; background-color: var(--text-secondary);">清空消息</button>
<div id="queryStatus" class="status"></div>
<div id="queryResult" class="result-container">
<h3>查询结果:</h3>
<div class="query-result" id="resultText"></div>
</div>
<div id="processResult" class="result-container">
<h3>处理过程:</h3>
<div id="messageStream" class="message-stream">
<div class="message-container" id="messageContainer"></div>
</div>
</div>
</div>
</main>
<footer>
<p>DeepSearcher © 2025 | 企业知识管理与智能问答系统</p>
</footer>
</div>
<script src="static/js/app.js"></script>
</body>
</html>

280
deepsearcher/templates/static/css/styles.css

@ -0,0 +1,280 @@
:root {
--primary-color: #4f46e5;
--secondary-color: #f9fafb;
--border-color: #e5e7eb;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--success-color: #10b981;
--error-color: #ef4444;
--warning-color: #f59e0b;
--info-color: #3b82f6;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: var(--text-primary);
background-color: #f3f4f6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
h1 {
color: var(--primary-color);
margin-bottom: 10px;
}
.app-description {
color: var(--text-secondary);
max-width: 800px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 24px;
margin-bottom: 24px;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 16px;
color: var(--text-primary);
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
input, textarea, select {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 16px;
transition: border-color 0.15s;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
}
button {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 6px;
padding: 12px 20px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.15s;
}
button:hover {
background-color: #4338ca;
}
button:disabled {
background-color: var(--border-color);
cursor: not-allowed;
}
.btn-secondary {
background-color: var(--text-secondary);
}
.btn-secondary:hover {
background-color: var(--text-primary);
}
.result-container {
margin-top: 20px;
padding: 16px;
border-radius: 6px;
background-color: var(--secondary-color);
display: none;
}
.result-container.visible {
display: block;
}
.status {
padding: 12px;
border-radius: 6px;
margin: 10px 0;
display: none;
}
.status.visible {
display: block;
}
.status-success {
background-color: #d1fae5;
color: var(--success-color);
border: 1px solid var(--success-color);
}
.status-error {
background-color: #fee2e2;
color: var(--error-color);
border: 1px solid var(--error-color);
}
.status-loading {
background-color: #fffbeb;
color: var(--warning-color);
border: 1px solid var(--warning-color);
display: flex;
align-items: center;
}
.loading-spinner {
display: none;
width: 12px;
height: 12px;
border: 2px solid #f3f3f3;
border-top: 2px solid var(--warning-color);
border-radius: 50%;
animation: spin 1s linear infinite;
flex-shrink: 0;
align-items: center;
}
.status-loading .loading-spinner {
display: inline-block;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading .loading-spinner {
display: inline-block;
}
.query-result {
white-space: pre-wrap;
line-height: 1.6;
background-color: white;
padding: 16px;
border-radius: 6px;
border: 1px solid var(--border-color);
margin-top: 8px;
}
.message-stream {
margin-top: 16px;
}
#processResult {
margin-top: 16px;
}
#processResult h3 {
color: var(--text-secondary);
font-size: 1rem;
}
#queryResult h3 {
color: var(--text-secondary);
font-size: 1rem;
}
.message-container {
border: 1px solid var(--border-color);
border-radius: 6px;
background-color: white;
padding: 12px;
}
.message {
margin-bottom: 12px;
padding: 8px 12px;
border-radius: 4px;
border-left: 4px solid;
font-size: 14px;
line-height: 1.4;
}
.message-start {
background-color: #f0f9ff;
border-left-color: var(--info-color);
}
.message-info {
background-color: #fef3c7;
border-left-color: var(--warning-color);
}
.message-answer {
background-color: #d1fae5;
border-left-color: var(--success-color);
}
.message-complete {
background-color: #dbeafe;
border-left-color: var(--primary-color);
}
.message-error {
background-color: #fee2e2;
border-left-color: var(--error-color);
}
.message-timestamp {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
}
footer {
text-align: center;
margin-top: 30px;
padding: 20px;
color: var(--text-secondary);
font-size: 0.875rem;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.card {
padding: 16px;
}
}

446
deepsearcher/templates/static/js/app.js

@ -0,0 +1,446 @@
// 全局变量
let eventSource = null;
let isStreaming = false;
// 工具函数:显示状态信息
function showStatus(elementId, message, type) {
const statusElement = document.getElementById(elementId);
// 清除之前的类型类
statusElement.classList.remove('status-success', 'status-error', 'status-loading');
// 添加新的类型类
if (type === 'success') {
statusElement.classList.add('status-success');
statusElement.innerHTML = message;
} else if (type === 'error') {
statusElement.classList.add('status-error');
statusElement.innerHTML = message;
} else if (type === 'loading') {
statusElement.classList.add('status-loading');
statusElement.innerHTML = `<div class="loading-spinner"></div>${message}`;
}
statusElement.classList.add('visible');
}
// 工具函数:显示消息流
function displayMessages(messages) {
const container = document.getElementById('messageContainer');
container.innerHTML = '';
messages.forEach(message => {
addMessageToContainer(message);
});
// 滚动到底部
container.scrollTop = container.scrollHeight;
}
// 工具函数:添加单个消息到容器
function addMessageToContainer(message) {
console.log('Adding message to container:', message);
const container = document.getElementById('messageContainer');
if (!container) {
console.error('Message container not found!');
return;
}
const messageElement = document.createElement('div');
messageElement.className = `message message-${message.type}`;
const contentElement = document.createElement('div');
contentElement.textContent = message.content;
messageElement.appendChild(contentElement);
// 只有在有有效时间戳时才显示时间
if (message.timestamp && !isNaN(message.timestamp)) {
const date = new Date(message.timestamp * 1000);
if (!isNaN(date.getTime())) {
const timestampElement = document.createElement('div');
timestampElement.className = 'message-timestamp';
timestampElement.textContent = date.toLocaleTimeString();
messageElement.appendChild(timestampElement);
}
}
container.appendChild(messageElement);
// 确保处理过程容器是可见的
const processContainer = document.getElementById('processResult');
if (processContainer && !processContainer.classList.contains('visible')) {
processContainer.classList.add('visible');
}
// 滚动到底部
container.scrollTop = container.scrollHeight;
console.log('Message added successfully, container now has', container.children.length, 'messages');
}
// 工具函数:隐藏状态信息
function hideStatus(elementId) {
const statusElement = document.getElementById(elementId);
statusElement.classList.remove('visible');
}
// 工具函数:显示结果
function showResult() {
const resultElement = document.getElementById('queryResult');
resultElement.classList.add('visible');
}
// 工具函数:隐藏结果
function hideResult() {
const resultElement = document.getElementById('queryResult');
resultElement.classList.remove('visible');
}
// 工具函数:显示处理过程
function showProcessResult() {
const processElement = document.getElementById('processResult');
processElement.classList.add('visible');
}
// 工具函数:隐藏处理过程
function hideProcessResult() {
const processElement = document.getElementById('processResult');
processElement.classList.remove('visible');
}
// 工具函数:转义HTML特殊字符
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
// 工具函数:设置按钮加载状态
function setButtonLoading(button, loading) {
if (loading) {
button.classList.add('loading');
button.disabled = true;
} else {
button.classList.remove('loading');
button.disabled = false;
}
}
// 工具函数:关闭EventSource连接
function closeEventSource() {
if (eventSource) {
console.log('Closing eventSource in closeEventSource function');
eventSource.close();
eventSource = null;
}
if (window.currentEventSource) {
console.log('Closing currentEventSource in closeEventSource function');
window.currentEventSource.close();
window.currentEventSource = null;
}
isStreaming = false;
}
// 工具函数:处理实时消息流
function handleStreamMessage(data) {
try {
const message = JSON.parse(data);
switch (message.type) {
case 'connection':
console.log('Connected to message stream:', message.message);
break;
case 'heartbeat':
// 心跳消息,不需要处理
break;
case 'start':
console.log('Query started:', message.content);
showStatus('queryStatus', ' 正在处理...', 'loading');
addMessageToContainer(message);
break;
case 'complete':
console.log('Query completed - closing connection');
showStatus('queryStatus', '查询完成', 'success');
addMessageToContainer(message);
// 关闭EventSource连接
if (window.currentEventSource) {
console.log('Closing currentEventSource');
window.currentEventSource.close();
window.currentEventSource = null;
}
isStreaming = false;
setButtonLoading(document.getElementById('queryBtn'), false);
console.log('Query completed - connection closed, isStreaming set to false');
break;
case 'error':
console.error('Error:', message.content);
showStatus('queryStatus', message.content, 'error');
addMessageToContainer(message);
// 关闭EventSource连接
if (window.currentEventSource) {
window.currentEventSource.close();
window.currentEventSource = null;
}
isStreaming = false;
setButtonLoading(document.getElementById('queryBtn'), false);
break;
case 'info':
// 处理信息消息
console.log('Processing info message:', message.content.substring(0, 100) + '...');
addMessageToContainer(message);
break;
case 'answer':
// 处理answer类型,显示查询结果
console.log('Processing answer message:', message.content.substring(0, 100) + '...');
// 将结果内容显示在结果区域
if (message.content && message.content !== "==== FINAL ANSWER====") {
document.getElementById('resultText').textContent = message.content;
showResult();
}
// 不将answer消息添加到处理过程容器中,只显示在查询结果框中
break;
default:
console.log('Unknown message type:', message.type);
}
} catch (error) {
console.error('Error parsing message:', error);
}
}
// 工具函数:开始实时消息流
function startMessageStream() {
closeEventSource(); // 关闭之前的连接
eventSource = new EventSource('/stream-messages/');
eventSource.onopen = function(event) {
console.log('EventSource connection opened');
};
eventSource.onmessage = function(event) {
handleStreamMessage(event.data);
};
eventSource.onerror = function(event) {
console.error('EventSource error:', event);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('EventSource connection closed');
}
};
}
// 加载文件功能
document.getElementById('loadFilesBtn').addEventListener('click', async function() {
const button = this;
const filePathsInput = document.getElementById('filePaths').value;
const collectionName = document.getElementById('collectionName').value;
const collectionDesc = document.getElementById('collectionDesc').value;
if (!filePathsInput) {
showStatus('loadStatus', '请提供至少一个文件路径', 'error');
return;
}
const filePaths = filePathsInput.split(',').map(path => path.trim()).filter(path => path);
setButtonLoading(button, true);
showStatus('loadStatus', '正在加载文件...', 'loading');
hideResult();
hideProcessResult();
try {
const response = await fetch('/load-files/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
paths: filePaths,
collection_name: collectionName || undefined,
collection_description: collectionDesc || undefined
})
});
const data = await response.json();
if (response.ok) {
showStatus('loadStatus', data.message, 'success');
} else {
showStatus('loadStatus', `加载失败: ${data.detail}`, 'error');
}
} catch (error) {
showStatus('loadStatus', `请求失败: ${error.message}`, 'error');
} finally {
setButtonLoading(button, false);
}
});
// 清空消息功能
document.getElementById('clearMessagesBtn').addEventListener('click', async function() {
try {
const response = await fetch('/clear-messages/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
// 清空消息容器
const container = document.getElementById('messageContainer');
container.innerHTML = '';
// 清空查询结果
const resultText = document.getElementById('resultText');
resultText.textContent = '';
// 隐藏处理过程容器
hideProcessResult();
// 隐藏查询结果容器
hideResult();
showStatus('queryStatus', '消息已清空', 'success');
} else {
showStatus('queryStatus', '清空消息失败', 'error');
}
} catch (error) {
showStatus('queryStatus', `请求失败: ${error.message}`, 'error');
}
});
// 加载网站内容功能
document.getElementById('loadWebsiteBtn').addEventListener('click', async function() {
const button = this;
const urlsInput = document.getElementById('websiteUrls').value;
const collectionName = document.getElementById('webCollectionName').value;
const collectionDesc = document.getElementById('webCollectionDesc').value;
if (!urlsInput) {
showStatus('webLoadStatus', '请提供至少一个网站URL', 'error');
return;
}
const urls = urlsInput.split(',').map(url => url.trim()).filter(url => url);
setButtonLoading(button, true);
showStatus('webLoadStatus', '正在加载网站内容...', 'loading');
hideResult();
hideProcessResult();
try {
const response = await fetch('/load-website/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
urls: urls,
collection_name: collectionName || undefined,
collection_description: collectionDesc || undefined
})
});
const data = await response.json();
if (response.ok) {
showStatus('webLoadStatus', data.message, 'success');
} else {
showStatus('webLoadStatus', `加载失败: ${data.detail}`, 'error');
}
} catch (error) {
showStatus('webLoadStatus', `请求失败: ${error.message}`, 'error');
} finally {
setButtonLoading(button, false);
}
});
// 查询功能 - 使用实时流
document.getElementById('queryBtn').addEventListener('click', async function() {
const button = this;
const queryText = document.getElementById('queryText').value;
const maxIter = parseInt(document.getElementById('maxIter').value);
if (!queryText) {
showStatus('queryStatus', '请输入查询问题', 'error');
return;
}
if (isNaN(maxIter) || maxIter < 1 || maxIter > 10) {
showStatus('queryStatus', '迭代次数必须是1到10之间的整数', 'error');
return;
}
if (isStreaming) {
console.log('Query already in progress, isStreaming:', isStreaming);
showStatus('queryStatus', '查询正在进行中,请等待完成', 'error');
return;
}
setButtonLoading(button, true);
showStatus('queryStatus', '正在启动查询...', 'loading');
hideResult();
hideProcessResult();
// 清空消息容器
const container = document.getElementById('messageContainer');
container.innerHTML = '';
try {
console.log('Starting new query, setting isStreaming to true');
isStreaming = true;
// 确保没有其他连接存在
if (window.currentEventSource) {
console.log('Closing existing EventSource connection');
window.currentEventSource.close();
window.currentEventSource = null;
}
// 使用EventSource直接连接到查询流
const eventSource = new EventSource(`/query-stream/?original_query=${encodeURIComponent(queryText)}&max_iter=${maxIter}`);
// 保存EventSource引用以便后续关闭
window.currentEventSource = eventSource;
eventSource.onopen = function(event) {
console.log('EventSource connection opened for query');
showStatus('queryStatus', ' 正在处理...', 'loading');
};
eventSource.onmessage = function(event) {
console.log('Received message:', event.data);
handleStreamMessage(event.data);
};
eventSource.onerror = function(event) {
console.error('EventSource error:', event);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('EventSource connection closed due to error');
isStreaming = false;
setButtonLoading(button, false);
window.currentEventSource = null;
}
};
} catch (error) {
console.error('Query error:', error);
showStatus('queryStatus', `请求失败: ${error.message}`, 'error');
isStreaming = false;
setButtonLoading(button, false);
}
});
// 页面卸载时清理连接
window.addEventListener('beforeunload', function() {
if (window.currentEventSource) {
window.currentEventSource.close();
window.currentEventSource = null;
}
});

8
main.py

@ -4,6 +4,7 @@ import uvicorn
from fastapi import Body, FastAPI, HTTPException, Query from fastapi import Body, FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, StreamingResponse from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel from pydantic import BaseModel
import os import os
import asyncio import asyncio
@ -18,6 +19,11 @@ from deepsearcher.utils.message_stream import get_message_stream
app = FastAPI() app = FastAPI()
# 配置静态文件服务
current_dir = os.path.dirname(os.path.abspath(__file__))
static_dir = os.path.join(current_dir, "deepsearcher", "templates", "static")
app.mount("/static", StaticFiles(directory=static_dir), name="static")
config = Configuration() config = Configuration()
init_config(config) init_config(config)
@ -47,7 +53,7 @@ async def read_root():
Serve the main HTML page. Serve the main HTML page.
""" """
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
template_path = os.path.join(current_dir, "deepsearcher", "backend", "templates", "index.html") template_path = os.path.join(current_dir, "deepsearcher", "templates", "html", "index.html")
try: try:
with open(template_path, encoding="utf-8") as file: with open(template_path, encoding="utf-8") as file:

38
test_fix.py

@ -1,38 +0,0 @@
#!/usr/bin/env python3
"""
测试修复后的查询功能
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from deepsearcher.configuration import Configuration, init_config
from deepsearcher.online_query import query
def test_query_fix():
"""测试修复后的查询功能"""
print("=== 测试修复后的查询功能 ===")
# 初始化配置
config = Configuration()
init_config(config)
try:
print("开始查询...")
result_text, retrieval_results = query("什么是Milvus?", max_iter=1)
print(f"查询完成!")
print(f"结果长度: {len(result_text) if result_text else 0}")
print(f"检索结果数量: {len(retrieval_results) if retrieval_results else 0}")
if result_text:
print(f"结果预览: {result_text[:200]}...")
except Exception as e:
import traceback
print(f"查询失败: {e}")
print(f"错误详情: {traceback.format_exc()}")
if __name__ == "__main__":
test_query_fix()
Loading…
Cancel
Save