feat: 添加配置管理功能和修复搜索问题

新增:
- 系统设置页面 (/settings) - 支持动态配置LLM、索引、文档处理参数
- 配置API - 保存配置、测试LLM连接
- 前端JS交互文件 - 搜索、文档管理功能

修复:
- 首页搜索框无法正常工作的问题(缺少main.js)
- 服务支持动态读取配置(无需重启生效)

改进:
- LLM/索引/文档配置支持热更新
- 添加测试LLM连接功能
This commit is contained in:
2026-04-09 12:54:31 +08:00
parent 4fb4d61877
commit 8c7a99d83f
6 changed files with 712 additions and 24 deletions

4
.gitignore vendored
View File

@@ -2,5 +2,7 @@ instance/
*.db
__pycache__/
*.pyc
.envdocuments/
.env
documents/
indexes/
user_config.json

103
app.py
View File

@@ -13,6 +13,38 @@ from config import *
from models import db, Document, DocumentChunk, InvertedIndex, QueryLog, IndexStats
from services import DocumentIndexer, SearchEngine, RAGGenerator
# ==================== 配置文件路径 ====================
CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'config.py')
USER_CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'user_config.json')
def load_user_config():
"""加载用户配置"""
if os.path.exists(USER_CONFIG_FILE):
with open(USER_CONFIG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
def save_user_config(config_type, config_data):
"""保存用户配置"""
user_config = load_user_config()
user_config[config_type] = config_data
with open(USER_CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(user_config, f, ensure_ascii=False, indent=2)
def get_effective_config():
"""获取有效配置(用户配置覆盖默认配置)"""
user_config = load_user_config()
return {
'llm': {**LLM_CONFIG, **user_config.get('llm', {})},
'index': {**INDEX_CONFIG, **user_config.get('index', {})},
'doc': {**DOC_CONFIG, **user_config.get('doc', {})}
}
# ==================== 创建应用 ====================
app = Flask(__name__)
app.config['SECRET_KEY'] = SECRET_KEY
@@ -66,6 +98,13 @@ def search_page():
return render_template('search.html')
@app.route('/settings')
def settings_page():
"""设置页"""
config = get_effective_config()
return render_template('settings.html', config=config)
# ==================== API路由 ====================
# === 文档管理 ===
@@ -315,6 +354,70 @@ def api_log_feedback(log_id):
return jsonify({'success': True})
# === 配置管理 ===
@app.route('/api/config', methods=['GET'])
def api_get_config():
"""获取当前配置"""
return jsonify(get_effective_config())
@app.route('/api/config/llm', methods=['POST'])
def api_save_llm_config():
"""保存LLM配置"""
data = request.json
save_user_config('llm', data)
return jsonify({'success': True})
@app.route('/api/config/index', methods=['POST'])
def api_save_index_config():
"""保存索引配置"""
data = request.json
save_user_config('index', data)
return jsonify({'success': True})
@app.route('/api/config/doc', methods=['POST'])
def api_save_doc_config():
"""保存文档配置"""
data = request.json
save_user_config('doc', data)
return jsonify({'success': True})
@app.route('/api/config/test', methods=['POST'])
def api_test_config():
"""测试LLM连接"""
config = get_effective_config()
llm_config = config['llm']
try:
from openai import OpenAI
client = OpenAI(
api_key=llm_config['api_key'],
base_url=llm_config['api_base'],
)
# 发送简单测试请求
response = client.chat.completions.create(
model=llm_config['model'],
messages=[{"role": "user", "content": "Hello"}],
max_tokens=10
)
return jsonify({
'success': True,
'model': llm_config['model'],
'response': response.choices[0].message.content
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
})
# ==================== 启动 ====================
if __name__ == '__main__':
init_app()

View File

@@ -16,17 +16,53 @@ from config import LLM_CONFIG, DOC_CONFIG, INDEX_CONFIG
from models import db, Document, DocumentChunk, InvertedIndex, IndexStats
def get_llm_config():
"""获取有效的LLM配置支持动态更新"""
user_config_file = os.path.join(os.path.dirname(__file__), 'user_config.json')
if os.path.exists(user_config_file):
with open(user_config_file, 'r', encoding='utf-8') as f:
user_config = json.load(f)
return {**LLM_CONFIG, **user_config.get('llm', {})}
return LLM_CONFIG
def get_doc_config():
"""获取有效的文档配置"""
user_config_file = os.path.join(os.path.dirname(__file__), 'user_config.json')
if os.path.exists(user_config_file):
with open(user_config_file, 'r', encoding='utf-8') as f:
user_config = json.load(f)
return {**DOC_CONFIG, **user_config.get('doc', {})}
return DOC_CONFIG
def get_index_config():
"""获取有效的索引配置"""
user_config_file = os.path.join(os.path.dirname(__file__), 'user_config.json')
if os.path.exists(user_config_file):
with open(user_config_file, 'r', encoding='utf-8') as f:
user_config = json.load(f)
return {**INDEX_CONFIG, **user_config.get('index', {})}
return INDEX_CONFIG
class LLMService:
"""LLM服务封装"""
def __init__(self):
self.client = OpenAI(
api_key=LLM_CONFIG['api_key'],
base_url=LLM_CONFIG['api_base'],
pass # 不再在初始化时设置配置
def _get_client(self):
"""获取LLM客户端"""
config = get_llm_config()
return OpenAI(
api_key=config['api_key'],
base_url=config['api_base'],
)
self.model = LLM_CONFIG['model']
self.max_tokens = LLM_CONFIG['max_tokens']
self.temperature = LLM_CONFIG['temperature']
def _get_config(self):
"""获取当前配置"""
return get_llm_config()
def analyze_document(self, content, title=None):
"""
@@ -60,8 +96,10 @@ class LLMService:
只返回JSON不要其他内容。"""
try:
response = self.client.chat.completions.create(
model=self.model,
config = self._get_config()
client = self._get_client()
response = client.chat.completions.create(
model=config['model'],
messages=[{"role": "user", "content": prompt}],
max_tokens=1000,
temperature=0.3,
@@ -106,8 +144,10 @@ class LLMService:
只返回JSON。"""
try:
response = self.client.chat.completions.create(
model=self.model,
config = self._get_config()
client = self._get_client()
response = client.chat.completions.create(
model=config['model'],
messages=[{"role": "user", "content": prompt}],
max_tokens=500,
temperature=0.3,
@@ -150,8 +190,10 @@ class LLMService:
只返回JSON。"""
try:
response = self.client.chat.completions.create(
model=self.model,
config = self._get_config()
client = self._get_client()
response = client.chat.completions.create(
model=config['model'],
messages=[{"role": "user", "content": prompt}],
max_tokens=500,
temperature=0.3,
@@ -177,8 +219,11 @@ class DocumentIndexer:
def __init__(self):
self.llm = LLMService()
self.chunk_size = DOC_CONFIG['chunk_size']
self.chunk_overlap = DOC_CONFIG['chunk_overlap']
def _get_chunk_config(self):
"""获取分块配置"""
config = get_doc_config()
return config['chunk_size'], config['chunk_overlap']
def index_document(self, doc_id):
"""
@@ -330,6 +375,7 @@ class DocumentIndexer:
Returns:
list: 内容块列表
"""
chunk_size, _ = self._get_chunk_config()
chunks = []
# 按段落分割
@@ -337,7 +383,7 @@ class DocumentIndexer:
current_chunk = ""
for para in paragraphs:
if len(current_chunk) + len(para) < self.chunk_size:
if len(current_chunk) + len(para) < chunk_size:
current_chunk += para + '\n\n'
else:
if current_chunk.strip():
@@ -347,7 +393,7 @@ class DocumentIndexer:
if current_chunk.strip():
chunks.append(current_chunk.strip())
return chunks if chunks else [content[:self.chunk_size]]
return chunks if chunks else [content[:chunk_size]]
def _compute_term_freq(self, content):
"""计算词频"""
@@ -424,8 +470,11 @@ class SearchEngine:
def __init__(self):
self.llm = LLMService()
self.k1 = INDEX_CONFIG['bm25_k1']
self.b = INDEX_CONFIG['bm25_b']
def _get_bm25_params(self):
"""获取BM25参数"""
config = get_index_config()
return config['bm25_k1'], config['bm25_b']
def search(self, query, top_k=10):
"""
@@ -542,6 +591,7 @@ class SearchEngine:
continue
# BM25计算
k1, b = self._get_bm25_params()
score = 0
doc_len = doc.word_count or 1000
@@ -557,8 +607,8 @@ class SearchEngine:
tf = data['terms'].get(term, 0)
# BM25公式
tf_component = (tf * (self.k1 + 1)) / (
tf + self.k1 * (1 - self.b + self.b * doc_len / avg_doc_len)
tf_component = (tf * (k1 + 1)) / (
tf + k1 * (1 - b + b * doc_len / avg_doc_len)
)
score += idf * tf_component
@@ -653,13 +703,14 @@ class RAGGenerator:
请给出准确、简洁的回答,并标注信息来源。"""
try:
llm_config = get_llm_config()
client = OpenAI(
api_key=LLM_CONFIG['api_key'],
base_url=LLM_CONFIG['api_base'],
api_key=llm_config['api_key'],
base_url=llm_config['api_base'],
)
response = client.chat.completions.create(
model=LLM_CONFIG['model'],
model=llm_config['model'],
messages=[{"role": "user", "content": prompt}],
max_tokens=1000,
temperature=0.5,

247
static/js/main.js Normal file
View File

@@ -0,0 +1,247 @@
/**
* LLM Index RAG - 前端交互脚本
*/
// 搜索表单处理
document.getElementById('searchForm')?.addEventListener('submit', async function(e) {
e.preventDefault();
const query = document.getElementById('queryInput').value.trim();
const mode = document.querySelector('input[name="mode"]:checked').value;
if (!query) {
alert('请输入查询内容');
return;
}
// 显示加载状态
const resultsSection = document.getElementById('resultsSection');
const ragSection = document.getElementById('ragSection');
const resultsContainer = document.getElementById('resultsContainer');
const ragAnswer = document.getElementById('ragAnswer');
const ragSources = document.getElementById('ragSources');
const resultCount = document.getElementById('resultCount');
resultsSection.style.display = 'none';
ragSection.style.display = 'none';
resultsContainer.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary"></div><p class="mt-2">正在检索...</p></div>';
resultsSection.style.display = 'block';
try {
if (mode === 'search') {
// 文档检索模式
const response = await fetch('/api/search', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query: query, top_k: 10})
});
const data = await response.json();
if (data.error) {
resultsContainer.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
return;
}
resultCount.textContent = data.total;
if (data.results && data.results.length > 0) {
resultsContainer.innerHTML = data.results.map(r => `
<div class="result-item">
<h6 class="mb-1">
<a href="/documents" class="text-decoration-none">${r.title || r.document_title || '文档'}</a>
</h6>
<p class="text-muted small mb-1">${r.summary || r.content?.substring(0, 200) + '...' || ''}</p>
<div class="d-flex gap-2 align-items-center">
<span class="source-tag">${r.source || '本地文档'}</span>
<span class="badge bg-primary">${(r.score * 100).toFixed(1)}%</span>
</div>
</div>
`).join('');
} else {
resultsContainer.innerHTML = `
<div class="text-center py-4 text-muted">
<i class="bi bi-search display-4"></i>
<p class="mt-2">未找到相关结果</p>
<p class="small">请尝试其他关键词,或先上传并索引文档</p>
</div>
`;
}
} else {
// RAG问答模式
resultsContainer.innerHTML = '<div class="text-center py-4"><div class="spinner-border text-primary"></div><p class="mt-2">正在生成回答...</p></div>';
const response = await fetch('/api/rag/answer', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query: query, top_k: 5})
});
const data = await response.json();
if (data.error) {
resultsContainer.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
return;
}
resultsSection.style.display = 'none';
ragSection.style.display = 'block';
// 显示回答
ragAnswer.innerHTML = `
<div class="d-flex align-items-start gap-3">
<div class="bg-primary text-white rounded-circle p-2">
<i class="bi bi-robot"></i>
</div>
<div class="flex-grow-1">
<div class="markdown-content">${data.answer || '抱歉,无法生成回答。'}</div>
</div>
</div>
`;
// 显示来源
if (data.sources && data.sources.length > 0) {
ragSources.innerHTML = data.sources.map(s => `
<div class="result-item">
<h6 class="mb-1">${s.title || s.document_title || '参考文档'}</h6>
<p class="text-muted small mb-0">${s.content?.substring(0, 150) + '...' || ''}</p>
</div>
`).join('');
} else {
ragSources.innerHTML = '<p class="text-muted">无参考来源</p>';
}
}
} catch (err) {
resultsContainer.innerHTML = `<div class="alert alert-danger">请求失败: ${err.message}</div>`;
}
});
// 文档上传
document.getElementById('uploadForm')?.addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const fileInput = document.getElementById('fileInput');
if (!fileInput.files.length) {
alert('请选择文件');
return;
}
const uploadBtn = document.getElementById('uploadBtn');
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 上传中...';
try {
const response = await fetch('/api/documents', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
alert('上传成功!');
location.reload();
} else {
alert('上传失败: ' + (data.error || '未知错误'));
}
} catch (err) {
alert('上传失败: ' + err.message);
} finally {
uploadBtn.disabled = false;
uploadBtn.innerHTML = '<i class="bi bi-upload"></i> 上传';
}
});
// 索引文档
async function indexDocument(docId) {
if (!confirm('确定要索引此文档吗?这可能需要一些时间。')) return;
try {
const response = await fetch(`/api/index/${docId}`, {method: 'POST'});
const data = await response.json();
if (data.success) {
alert('索引完成!');
location.reload();
} else {
alert('索引失败: ' + (data.error || '未知错误'));
}
} catch (err) {
alert('索引失败: ' + err.message);
}
}
// 删除文档
async function deleteDocument(docId) {
if (!confirm('确定要删除此文档吗?此操作不可恢复。')) return;
try {
const response = await fetch(`/api/documents/${docId}`, {method: 'DELETE'});
const data = await response.json();
if (data.success) {
alert('删除成功!');
location.reload();
} else {
alert('删除失败: ' + (data.error || '未知错误'));
}
} catch (err) {
alert('删除失败: ' + err.message);
}
}
// 批量索引
async function batchIndex() {
if (!confirm('确定要索引所有待处理文档吗?')) return;
try {
const response = await fetch('/api/index/batch', {method: 'POST'});
const data = await response.json();
alert(`索引完成!成功: ${data.success}, 失败: ${data.failed}`);
location.reload();
} catch (err) {
alert('批量索引失败: ' + err.message);
}
}
// 重建索引
async function rebuildIndex() {
if (!confirm('重建索引将清除所有现有索引,确定继续吗?')) return;
try {
const response = await fetch('/api/index/rebuild', {method: 'POST'});
const data = await response.json();
alert(`重建完成!成功: ${data.success}, 失败: ${data.failed}`);
location.reload();
} catch (err) {
alert('重建索引失败: ' + err.message);
}
}
// 加载统计信息
async function loadStats() {
try {
const response = await fetch('/api/stats');
const stats = await response.json();
document.getElementById('statDocs').textContent = stats.total_documents || 0;
document.getElementById('statChunks').textContent = stats.total_chunks || 0;
document.getElementById('statTerms').textContent = stats.total_terms || 0;
document.getElementById('statWords').textContent = (stats.total_words || 0).toLocaleString();
} catch (err) {
console.error('加载统计失败:', err);
}
}
// 页面加载时刷新统计
document.addEventListener('DOMContentLoaded', function() {
// 如果在首页,定时刷新统计
if (document.getElementById('statDocs')) {
loadStats();
setInterval(loadStats, 30000); // 每30秒刷新
}
});

View File

@@ -28,6 +28,7 @@
<a class="nav-link" href="/">首页</a>
<a class="nav-link" href="/documents">文档管理</a>
<a class="nav-link" href="/search">知识检索</a>
<a class="nav-link" href="/settings">系统设置</a>
</div>
</div>
</nav>

284
templates/settings.html Normal file
View File

@@ -0,0 +1,284 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统设置 - LLM Index RAG</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { background-color: #f8f9fa; }
.config-card { border-radius: 10px; border: none; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">
<i class="bi bi-search"></i> LLM Index RAG
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/">首页</a>
<a class="nav-link" href="/documents">文档管理</a>
<a class="nav-link" href="/search">知识检索</a>
<a class="nav-link active" href="/settings">系统设置</a>
</div>
</div>
</nav>
<div class="container py-4">
<h4 class="mb-4"><i class="bi bi-gear"></i> 系统设置</h4>
<!-- LLM配置 -->
<div class="card config-card mb-4">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-cpu text-primary"></i> 大模型配置</h5>
</div>
<div class="card-body">
<form id="llmConfigForm">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">API地址</label>
<input type="text" class="form-control" id="apiBase"
value="{{ config.llm.api_base }}" placeholder="http://localhost:1234/v1">
<small class="text-muted">LLM API的基础URL</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">API Key</label>
<input type="text" class="form-control" id="apiKey"
value="{{ config.llm.api_key }}" placeholder="sk-xxx">
<small class="text-muted">API密钥</small>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">模型名称</label>
<input type="text" class="form-control" id="model"
value="{{ config.llm.model }}" placeholder="qwen/qwen3.5-35b-a3b">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Max Tokens</label>
<input type="number" class="form-control" id="maxTokens"
value="{{ config.llm.max_tokens }}">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Temperature</label>
<input type="number" class="form-control" id="temperature"
value="{{ config.llm.temperature }}" step="0.1" min="0" max="2">
</div>
</div>
<div class="mb-3">
<label class="form-label">超时时间(秒)</label>
<input type="number" class="form-control" id="timeout"
value="{{ config.llm.timeout }}">
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> 保存配置
</button>
<button type="button" class="btn btn-outline-secondary" onclick="testConnection()">
<i class="bi bi-plug"></i> 测试连接
</button>
</div>
</form>
<div id="testResult" class="mt-3"></div>
</div>
</div>
<!-- 索引配置 -->
<div class="card config-card mb-4">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-list-columns text-success"></i> 索引配置</h5>
</div>
<div class="card-body">
<form id="indexConfigForm">
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">BM25 K1参数</label>
<input type="number" class="form-control" id="bm25K1"
value="{{ config.index.bm25_k1 }}" step="0.1">
<small class="text-muted">词频饱和参数推荐1.2-2.0</small>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">BM25 B参数</label>
<input type="number" class="form-control" id="bm25B"
value="{{ config.index.bm25_b }}" step="0.05">
<small class="text-muted">文档长度归一化推荐0.75</small>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">最大返回结果</label>
<input type="number" class="form-control" id="maxResults"
value="{{ config.index.max_results }}">
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">标题权重</label>
<input type="number" class="form-control" id="titleWeight"
value="{{ config.index.title_weight }}" step="0.5">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">关键词权重</label>
<input type="number" class="form-control" id="keywordWeight"
value="{{ config.index.keyword_weight }}" step="0.5">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">内容权重</label>
<input type="number" class="form-control" id="contentWeight"
value="{{ config.index.content_weight }}" step="0.5">
</div>
</div>
<button type="submit" class="btn btn-success">
<i class="bi bi-save"></i> 保存配置
</button>
</form>
</div>
</div>
<!-- 文档配置 -->
<div class="card config-card">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-file-earmark-text text-warning"></i> 文档处理配置</h5>
</div>
<div class="card-body">
<form id="docConfigForm">
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">分块大小</label>
<input type="number" class="form-control" id="chunkSize"
value="{{ config.doc.chunk_size }}">
<small class="text-muted">字符数</small>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">分块重叠</label>
<input type="number" class="form-control" id="chunkOverlap"
value="{{ config.doc.chunk_overlap }}">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">最大关键词数</label>
<input type="number" class="form-control" id="maxKeywords"
value="{{ config.doc.max_keywords }}">
</div>
</div>
<button type="submit" class="btn btn-warning">
<i class="bi bi-save"></i> 保存配置
</button>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 保存LLM配置
document.getElementById('llmConfigForm')?.addEventListener('submit', async function(e) {
e.preventDefault();
const config = {
api_base: document.getElementById('apiBase').value,
api_key: document.getElementById('apiKey').value,
model: document.getElementById('model').value,
max_tokens: parseInt(document.getElementById('maxTokens').value),
temperature: parseFloat(document.getElementById('temperature').value),
timeout: parseInt(document.getElementById('timeout').value)
};
try {
const res = await fetch('/api/config/llm', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
});
const data = await res.json();
if (data.success) {
alert('LLM配置已保存');
} else {
alert('保存失败: ' + data.error);
}
} catch (err) {
alert('保存失败: ' + err.message);
}
});
// 测试连接
async function testConnection() {
const resultDiv = document.getElementById('testResult');
resultDiv.innerHTML = '<div class="alert alert-info"><span class="spinner-border spinner-border-sm"></span> 测试中...</div>';
try {
const res = await fetch('/api/config/test', {method: 'POST'});
const data = await res.json();
if (data.success) {
resultDiv.innerHTML = '<div class="alert alert-success"><i class="bi bi-check-circle"></i> 连接成功!模型: ' + data.model + '</div>';
} else {
resultDiv.innerHTML = '<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 连接失败: ' + data.error + '</div>';
}
} catch (err) {
resultDiv.innerHTML = '<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 连接失败: ' + err.message + '</div>';
}
}
// 保存索引配置
document.getElementById('indexConfigForm')?.addEventListener('submit', async function(e) {
e.preventDefault();
const config = {
bm25_k1: parseFloat(document.getElementById('bm25K1').value),
bm25_b: parseFloat(document.getElementById('bm25B').value),
max_results: parseInt(document.getElementById('maxResults').value),
title_weight: parseFloat(document.getElementById('titleWeight').value),
keyword_weight: parseFloat(document.getElementById('keywordWeight').value),
content_weight: parseFloat(document.getElementById('contentWeight').value)
};
try {
const res = await fetch('/api/config/index', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
});
const data = await res.json();
if (data.success) {
alert('索引配置已保存!');
} else {
alert('保存失败: ' + data.error);
}
} catch (err) {
alert('保存失败: ' + err.message);
}
});
// 保存文档配置
document.getElementById('docConfigForm')?.addEventListener('submit', async function(e) {
e.preventDefault();
const config = {
chunk_size: parseInt(document.getElementById('chunkSize').value),
chunk_overlap: parseInt(document.getElementById('chunkOverlap').value),
max_keywords: parseInt(document.getElementById('maxKeywords').value)
};
try {
const res = await fetch('/api/config/doc', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
});
const data = await res.json();
if (data.success) {
alert('文档配置已保存!');
} else {
alert('保存失败: ' + data.error);
}
} catch (err) {
alert('保存失败: ' + err.message);
}
});
</script>
</body>
</html>