htknow/src/config.rs
Defeng fe8f0a875a
Some checks failed
CI / Frontend (Vite) (push) Successful in 24s
CI / Docker Release Tar (push) Has been skipped
CI / Backend (Rust) (push) Failing after 10m15s
Initial project import
2026-07-24 10:28:31 +08:00

517 lines
18 KiB
Rust
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.

//! 配置管理模块
//!
//! 支持从环境变量读取配置,预留 etcd 支持接口,并支持实时更新。
use std::sync::{Arc, RwLock};
use once_cell::sync::Lazy;
/// 全局配置单例
///
/// 内层存 `Arc<AppConfig>``get()` 只克隆 Arc廉价避免每次深拷贝整个配置结构体。
/// 保留外层 `RwLock` 以支持未来的配置热更新(替换内层 Arc 即可)。
static CONFIG: Lazy<RwLock<Arc<AppConfig>>> = Lazy::new(|| RwLock::new(Arc::new(AppConfig::from_env())));
/// 获取当前配置的只读快照(克隆 Arc零深拷贝
pub fn get() -> Arc<AppConfig> {
CONFIG.read().unwrap().clone()
}
// 重新加载配置(用于热更新)
// pub fn reload() {
// let new_config = AppConfig::from_env();
// let mut config = CONFIG.write().unwrap();
// *config = new_config;
// info!("Configuration reloaded");
// }
// 使用新配置更新(用于 etcd watch 等场景)
// pub fn update(new_config: AppConfig) {
// let mut config = CONFIG.write().unwrap();
// *config = new_config;
// info!("Configuration updated");
// }
/// 应用配置
#[derive(Debug, Clone)]
pub struct AppConfig {
pub server: ServerConfig,
pub services: ServicesConfig,
pub ai: AIConfig,
pub database: DatabaseConfig,
pub storage: StorageConfig,
pub search: SearchConfig,
pub slice: SliceConfig,
pub llm: LLMConfig,
}
/// 服务器配置
#[derive(Debug, Clone)]
pub struct ServerConfig {
/// 监听地址
pub host: String,
/// 监听端口
pub port: u16,
/// 上传文件大小限制MB
pub upload_limit_mb: usize,
/// 文件处理间隔(秒)
pub process_interval_secs: u64,
/// 文件处理并发数
pub process_concurrency: usize,
/// LanceDB 自动压缩 cron 表达式off/disabled/0 表示禁用)
pub lancedb_compact_cron: String,
/// 是否启用后台文件解析
pub parse_enabled: bool,
/// 是否启用重复文件复用
pub reuse_duplicate_files: bool,
/// 文件解析后是否构建知识图谱
pub build_knowledge_graph: bool,
}
/// 外部服务配置
#[derive(Debug, Clone)]
pub struct ServicesConfig {
/// MinerU PDF 解析服务地址
pub mineru_url: String,
/// 外部接口请求超时(秒)
pub request_timeout_secs: u64,
/// MinerU 单次解析 PDF 最大页数0 表示不限制)
pub mineru_max_pages: usize,
/// Office 文档转 PDF 服务地址
pub office_convert_url: String,
/// 自定义解析服务地址(配置后仅 Word/PPT/PDF 走该服务)
pub custom_parse_url: Option<String>,
/// 自定义解析复用服务地址(仅输入 pdf_contents
pub custom_parse_reuse_url: Option<String>,
/// 音频转写服务地址
pub audio_transcription_url: String,
/// 音频转写服务 API Key可选
pub audio_transcription_key: Option<String>,
/// Embedding 服务地址
pub embedding_url: String,
/// 图片 Embedding 服务地址(可选,未配置时不进行图片 embedding
pub image_embedding_url: Option<String>,
/// Rerank 服务地址
pub rerank_url: String,
/// 图片文本化服务地址(可选)
pub image_parse_url: Option<String>,
/// 图片文本化服务超时(秒)
pub image_parse_timeout_secs: u64,
/// 图片文本化并发数
pub image_parse_concurrency: usize,
}
/// AI 模型配置
#[derive(Debug, Clone)]
pub struct AIConfig {
/// Embedding 模型名称
pub embedding_model: String,
/// Embedding 向量维度
pub embedding_dim: i32,
/// 图片 Embedding 向量维度
pub image_embedding_dim: i32,
/// Rerank 模型名称
pub rerank_model: String,
/// Rerank 分数阈值
pub rerank_threshold: f32,
/// Embedding 批量请求批次大小
pub embedding_batch_size: usize,
}
/// 数据库配置
#[derive(Debug, Clone)]
pub struct DatabaseConfig {
/// 数据库连接 URL
pub url: String,
/// 最大连接数
pub max_connections: u32,
/// 最小空闲连接数(保持热连接,避免负载波动时反复建连)
pub min_connections: u32,
/// 忙超时(毫秒)
pub busy_timeout_ms: u64,
/// 是否初始化默认知识库
pub init_default_kbs: bool,
}
/// 存储路径配置
#[derive(Debug, Clone)]
pub struct StorageConfig {
/// LanceDB 存储路径
pub lancedb_path: String,
/// 临时目录路径
pub temp_path: String,
/// 图片保存路径
pub images_path: String,
/// 转换后的 PDF 保存路径
pub pdf_path: String,
/// 上传文件保存路径
pub files_path: String,
/// 压缩文件解压后保存路径
pub archives_path: String,
/// 文件解析后完整文本的保存路径
pub contents_path: String,
/// 文件切片正文(按源文件聚合)的保存路径
pub slice_contents_path: String,
}
/// 搜索配置
#[derive(Debug, Clone)]
pub struct SearchConfig {
/// 搜索结果限制数
pub limit: usize,
/// Tantivy 索引路径
pub tantivy_index_path: String,
/// Tantivy 全文索引路径
pub tantivy_full_index_path: String,
/// Tantivy 索引内存MB
pub tantivy_memory_mb: usize,
/// Tantivy 重建批次大小
pub tantivy_rebuild_batch_size: usize,
/// LanceDB 从 SQLite 重建时的批次大小
pub lancedb_rebuild_batch_size: usize,
/// 文本 embedding 请求超时(秒)
pub embedding_timeout_secs: u64,
/// rerank 请求超时(秒)
pub rerank_timeout_secs: u64,
/// 是否启用同义词扩展
pub synonym_enabled: bool,
/// 同义词默认权重因子
pub synonym_boost: f32,
/// 每个词最多扩展的同义词数
pub max_synonyms_per_term: usize,
/// 单次查询最多扩展的同义词总数
pub max_total_synonyms: usize,
/// 高亮页码选择阈值:首选页的内容字数(按 pdf_content 中文本长度计算)少于该值时,优先使用第二页(如果存在)
pub highlight_page_min_chars: usize,
}
/// 切片配置
#[derive(Debug, Clone)]
pub struct SliceConfig {
/// 智能切片每个切片的最大字数
pub smart_slice_max_chars: usize,
/// 固定长度切片的重叠字数
pub fixed_slice_overlap_chars: usize,
}
/// LLM 配置
#[derive(Debug, Clone)]
pub struct LLMConfig {
/// LLM API URL
pub api_url: Option<String>,
/// LLM API Key
pub api_key: Option<String>,
/// LLM 模型名称
pub model: String,
}
impl AppConfig {
/// 从环境变量加载配置
pub fn from_env() -> Self {
Self {
server: ServerConfig::from_env(),
services: ServicesConfig::from_env(),
ai: AIConfig::from_env(),
database: DatabaseConfig::from_env(),
storage: StorageConfig::from_env(),
search: SearchConfig::from_env(),
slice: SliceConfig::from_env(),
llm: LLMConfig::from_env(),
}
}
}
impl ServerConfig {
fn from_env() -> Self {
Self {
host: env_or("HTKNOW_SERVER_HOST", "0.0.0.0"),
port: env_or_parse("HTKNOW_SERVER_PORT", 3000),
upload_limit_mb: env_or_parse("HTKNOW_SERVER_UPLOAD_LIMIT_MB", 500),
process_interval_secs: env_or_parse("HTKNOW_SERVER_PROCESS_INTERVAL_SECS", 10),
process_concurrency: env_or_parse("HTKNOW_SERVER_PROCESS_CONCURRENCY", 1),
lancedb_compact_cron: env_or("HTKNOW_LANCEDB_COMPACT_CRON", "0 0 3 * * *"),
parse_enabled: env_or_parse("HTKNOW_PARSE_ENABLED", true),
reuse_duplicate_files: env_or_parse("HTKNOW_REUSE_DUPLICATE_FILES", true),
build_knowledge_graph: env_or_parse("HTKNOW_BUILD_KNOWLEDGE_GRAPH", false),
}
}
}
impl ServicesConfig {
fn from_env() -> Self {
Self {
mineru_url: env_or("HTKNOW_MINERU_URL", "http://192.168.0.46:10001/file_parse"),
request_timeout_secs: env_or_parse("HTKNOW_REQUEST_TIMEOUT_SECS", 600),
mineru_max_pages: env_or_parse("HTKNOW_MINERU_MAX_PAGES", 50),
office_convert_url: env_or("HTKNOW_OFFICE_CONVERT_URL", "http://192.168.0.46:8003/convert"),
custom_parse_url: env_optional("HTKNOW_CUSTOM_PARSE_URL"),
custom_parse_reuse_url: env_optional("HTKNOW_CUSTOM_PARSE_REUSE_URL"),
audio_transcription_url: env_or(
"HTKNOW_AUDIO_TRANSCRIPTION_URL",
"http://192.168.0.46:59805/api/v1/audio/transcriptions",
),
audio_transcription_key: std::env::var("HTKNOW_AUDIO_TRANSCRIPTION_KEY").ok(),
embedding_url: env_or("HTKNOW_EMBEDDING_URL", "http://222.190.139.186:59700/v1/embeddings"),
image_embedding_url: env_optional("HTKNOW_IMAGE_EMBEDDING_URL"),
rerank_url: env_or("HTKNOW_RERANK_URL", "http://222.190.139.186:59600/v1/rerank"),
image_parse_url: env_optional("HTKNOW_IMAGE_PARSE_URL"),
image_parse_timeout_secs: env_or_parse("HTKNOW_IMAGE_PARSE_TIMEOUT_SECS", 120),
image_parse_concurrency: env_or_parse("HTKNOW_IMAGE_PARSE_CONCURRENCY", 5),
}
}
}
impl AIConfig {
fn from_env() -> Self {
let embedding_dim = env_or_parse("HTKNOW_EMBEDDING_DIM", 1024);
Self {
embedding_model: env_or("HTKNOW_EMBEDDING_MODEL", "bge-m3"),
embedding_dim,
image_embedding_dim: env_or_parse("HTKNOW_IMAGE_EMBEDDING_DIM", 2048),
rerank_model: env_or("HTKNOW_RERANK_MODEL", "bge-rerank"),
rerank_threshold: env_or_parse("HTKNOW_RERANK_THRESHOLD", 0.1),
embedding_batch_size: env_or_parse("HTKNOW_EMBEDDING_BATCH_SIZE", 8),
}
}
}
impl DatabaseConfig {
fn from_env() -> Self {
Self {
url: env_or("DATABASE_URL", "sqlite://data/app.sqlite"),
// 文件型 SQLite 写操作串行连接数过高只会加剧锁竞争WAL 下读可并发16 足够。
max_connections: env_or_parse("HTKNOW_DB_MAX_CONNECTIONS", 16),
min_connections: env_or_parse("HTKNOW_DB_MIN_CONNECTIONS", 2),
busy_timeout_ms: env_or_parse("HTKNOW_DB_BUSY_TIMEOUT_MS", 5000),
init_default_kbs: env_or_parse("HTKNOW_DB_INIT_DEFAULT_KBS", true),
}
}
}
impl StorageConfig {
fn from_env() -> Self {
let data_dir = env_or("HTKNOW_DATA_DIR", "data");
Self {
lancedb_path: env_or("HTKNOW_LANCEDB_PATH", &format!("{}/lancedb_data", data_dir)),
temp_path: env_or("HTKNOW_TEMP_PATH", &format!("{}/temp", data_dir)),
images_path: env_or("HTKNOW_IMAGES_PATH", &format!("{}/images", data_dir)),
pdf_path: env_or("HTKNOW_PDF_PATH", &format!("{}/pdfs", data_dir)),
files_path: env_or("HTKNOW_FILES_PATH", &format!("{}/files", data_dir)),
archives_path: env_or("HTKNOW_ARCHIVES_PATH", &format!("{}/archives", data_dir)),
contents_path: env_or("HTKNOW_CONTENTS_PATH", &format!("{}/contents", data_dir)),
slice_contents_path: env_or("HTKNOW_SLICE_CONTENTS_PATH", &format!("{}/slice_contents", data_dir)),
}
}
}
impl SearchConfig {
fn from_env() -> Self {
let data_dir = env_or("HTKNOW_DATA_DIR", "data");
Self {
limit: env_or_parse("HTKNOW_SEARCH_LIMIT", 10),
tantivy_index_path: env_or("HTKNOW_TANTIVY_INDEX_PATH", &format!("{}/tantivy_index", data_dir)),
tantivy_full_index_path: env_or(
"HTKNOW_TANTIVY_FULL_INDEX_PATH",
&format!("{}/tantivy_full_index", data_dir),
),
tantivy_memory_mb: env_or_parse("HTKNOW_TANTIVY_MEMORY_MB", 50),
tantivy_rebuild_batch_size: env_or_parse("HTKNOW_SEARCH_TANTIVY_REBUILD_BATCH_SIZE", 100),
lancedb_rebuild_batch_size: env_or_parse("HTKNOW_SEARCH_LANCEDB_REBUILD_BATCH_SIZE", 1000),
embedding_timeout_secs: env_or_parse("HTKNOW_SEARCH_EMBEDDING_TIMEOUT_SECS", 30),
rerank_timeout_secs: env_or_parse("HTKNOW_SEARCH_RERANK_TIMEOUT_SECS", 20),
synonym_enabled: env_or_parse("HTKNOW_SEARCH_SYNONYM_ENABLED", true),
synonym_boost: env_or_parse("HTKNOW_SEARCH_SYNONYM_BOOST", 0.7),
max_synonyms_per_term: env_or_parse("HTKNOW_SEARCH_MAX_SYNONYMS_PER_TERM", 5),
max_total_synonyms: env_or_parse("HTKNOW_SEARCH_MAX_TOTAL_SYNONYMS", 30),
highlight_page_min_chars: std::env::var("HTKNOW_HIGHLIGHT_PAGE_MIN_CHARS")
.ok()
.and_then(|v| v.parse().ok())
.or_else(|| std::env::var("HTKNOW_HIGHLIGHT_PAGE_MIN_POSITIONS").ok().and_then(|v| v.parse().ok()))
.unwrap_or(20),
}
}
}
impl SliceConfig {
fn from_env() -> Self {
Self {
smart_slice_max_chars: env_or_parse("HTKNOW_SMART_SLICE_MAX_CHARS", 8000),
fixed_slice_overlap_chars: env_or_parse("HTKNOW_FIXED_SLICE_OVERLAP_CHARS", 100),
}
}
}
impl LLMConfig {
fn from_env() -> Self {
Self {
api_url: std::env::var("LLM_API_URL").ok(),
api_key: std::env::var("LLM_API_KEY").ok(),
model: env_or("LLM_MODEL", "gpt-3.5-turbo"),
}
}
/// 检查 LLM 是否已启用
pub fn is_enabled(&self) -> bool {
self.api_url.is_some()
}
}
/// 从环境变量读取字符串,如果不存在则返回默认值
fn env_or(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.to_string())
}
/// 从环境变量读取字符串,空字符串视为未配置
fn env_optional(key: &str) -> Option<String> {
std::env::var(key).ok().and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }
})
}
/// 从环境变量读取并解析值,如果不存在或解析失败则返回默认值
fn env_or_parse<T: std::str::FromStr>(key: &str, default: T) -> T {
std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
}
// ============================================================================
// etcd 支持(可选功能)
// ============================================================================
/// 配置加载器 trait
#[allow(dead_code)]
pub trait ConfigLoader: Send+Sync {
/// 加载配置
fn load(&self) -> anyhow::Result<AppConfig>;
}
/// 环境变量配置加载器
#[allow(dead_code)]
pub struct EnvConfigLoader;
impl ConfigLoader for EnvConfigLoader {
fn load(&self) -> anyhow::Result<AppConfig> {
Ok(AppConfig::from_env())
}
}
/// etcd 配置加载器(预留接口)
#[cfg(feature = "etcd")]
#[allow(dead_code)]
pub struct EtcdConfigLoader {
pub endpoints: Vec<String>,
pub prefix: String,
}
#[cfg(feature = "etcd")]
#[allow(dead_code)]
impl EtcdConfigLoader {
pub fn new(endpoints: Vec<String>, prefix: String) -> Self {
Self { endpoints, prefix }
}
/// 启动 watch 监听配置变化
pub async fn watch<F>(&self, _callback: F) -> anyhow::Result<()>
where
F: Fn(AppConfig)+Send+Sync+'static, {
// TODO: 实现 etcd watch 逻辑
// 1. 连接 etcd
// 2. 监听 prefix 下的 key 变化
// 3. 当配置变化时调用 callback
unimplemented!("etcd watch not implemented yet")
}
}
#[cfg(feature = "etcd")]
impl ConfigLoader for EtcdConfigLoader {
fn load(&self) -> anyhow::Result<AppConfig> {
// TODO: 实现从 etcd 读取配置
// 1. 连接 etcd
// 2. 读取 prefix 下的所有 key
// 3. 解析并构建 AppConfig
// 4. 如果 etcd 中没有配置,回退到环境变量
unimplemented!("etcd config loading not implemented yet")
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use once_cell::sync::Lazy;
use super::*;
static ENV_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
#[test]
fn test_default_config() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe {
std::env::remove_var("HTKNOW_PARSE_ENABLED");
std::env::remove_var("HTKNOW_BUILD_KNOWLEDGE_GRAPH");
std::env::remove_var("HTKNOW_REQUEST_TIMEOUT_SECS");
}
let config = AppConfig::from_env();
// 验证默认值
assert_eq!(config.server.host, "0.0.0.0");
assert_eq!(config.server.port, 3000);
assert_eq!(config.server.upload_limit_mb, 500);
assert_eq!(config.ai.embedding_model, "bge-m3");
assert_eq!(config.ai.embedding_dim, 1024);
assert_eq!(config.ai.image_embedding_dim, 2048);
assert_eq!(config.services.request_timeout_secs, 600);
assert_eq!(config.search.embedding_timeout_secs, 30);
assert_eq!(config.search.rerank_timeout_secs, 20);
assert!(config.server.parse_enabled);
assert!(!config.server.build_knowledge_graph);
assert_eq!(config.search.highlight_page_min_chars, 20);
}
#[test]
fn test_get_config() {
let config = get();
assert!(!config.server.host.is_empty());
}
#[test]
fn test_parse_enabled_env_override() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("HTKNOW_PARSE_ENABLED", "false");
}
let config = AppConfig::from_env();
assert!(!config.server.parse_enabled);
unsafe {
std::env::remove_var("HTKNOW_PARSE_ENABLED");
}
}
#[test]
fn test_build_knowledge_graph_env_override() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("HTKNOW_BUILD_KNOWLEDGE_GRAPH", "true");
}
let config = AppConfig::from_env();
assert!(config.server.build_knowledge_graph);
unsafe {
std::env::remove_var("HTKNOW_BUILD_KNOWLEDGE_GRAPH");
}
}
#[test]
fn test_request_timeout_env_override() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("HTKNOW_REQUEST_TIMEOUT_SECS", "120");
}
let config = AppConfig::from_env();
assert_eq!(config.services.request_timeout_secs, 120);
unsafe {
std::env::remove_var("HTKNOW_REQUEST_TIMEOUT_SECS");
}
}
}