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

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>