Files
qiming/qiming-mcp-proxy/document-parser/src/config.rs
2026-06-01 13:03:20 +08:00

1506 lines
47 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.
use anyhow::Result;
use serde::{Deserialize, Deserializer, Serialize};
use std::env;
use std::fs::File;
use std::path::Path;
use std::str::FromStr;
use std::sync::{Arc, OnceLock, RwLock};
use thiserror::Error;
use tracing::{info, warn};
/// 默认配置文件内容
const DEFAULT_CONFIG_YAML: &str = include_str!("../config.yml");
/// 配置验证错误
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("配置文件读取失败: {0}")]
FileRead(String),
#[error("配置解析失败: {0}")]
Parse(String),
#[error("配置验证失败: {field} - {message}")]
Validation { field: String, message: String },
#[error("环境变量解析失败: {var} - {message}")]
EnvVar { var: String, message: String },
#[error("路径无效: {path} - {message}")]
InvalidPath { path: String, message: String },
}
/// 文件大小单位
#[derive(Debug, Clone)]
pub struct FileSize(pub u64);
impl<'de> Deserialize<'de> for FileSize {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
parse_file_size(&s)
.map(FileSize)
.map_err(serde::de::Error::custom)
}
}
impl FileSize {
pub fn bytes(&self) -> u64 {
self.0
}
pub fn mb(&self) -> u64 {
self.0 / (1024 * 1024)
}
pub fn from_mb(mb: u64) -> Self {
Self(mb * 1024 * 1024)
}
}
/// 全局文件大小配置
#[derive(Debug, Clone, Deserialize)]
pub struct GlobalFileSizeConfig {
/// 统一的最大文件大小限制
pub max_file_size: FileSize,
/// 大文档阈值(用于流式处理)
pub large_document_threshold: FileSize,
}
impl Default for GlobalFileSizeConfig {
fn default() -> Self {
Self {
max_file_size: FileSize(100 * 1024 * 1024), // 100MB
large_document_threshold: FileSize(1024 * 1024), // 1MB
}
}
}
impl GlobalFileSizeConfig {
/// 创建新的全局文件大小配置实例
pub fn new() -> Self {
get_global_file_size_config().clone()
}
/// 验证配置的有效性
pub fn validate(&self) -> Result<(), ConfigError> {
let configs = [
("max_file_size", self.max_file_size.bytes()),
(
"large_document_threshold",
self.large_document_threshold.bytes(),
),
];
for (name, size) in configs {
if size == 0 {
return Err(ConfigError::Validation {
field: format!("file_size_config.{name}"),
message: "文件大小不能为0".to_string(),
});
}
if size > 10 * 1024 * 1024 * 1024 {
// 10GB
return Err(ConfigError::Validation {
field: format!("file_size_config.{name}"),
message: "文件大小不能超过10GB".to_string(),
});
}
}
Ok(())
}
/// 获取指定用途的文件大小限制
pub fn get_max_size_for(&self, _purpose: FileSizePurpose) -> u64 {
// 统一使用同一个文件大小限制
self.max_file_size.bytes()
}
/// 检查文件大小是否超过指定用途的限制
pub fn is_size_allowed(&self, file_size: u64, purpose: FileSizePurpose) -> bool {
file_size <= self.get_max_size_for(purpose)
}
/// 获取大文档阈值
pub fn get_large_document_threshold(&self) -> u64 {
self.large_document_threshold.bytes()
}
/// 检查是否为大文档
pub fn is_large_document(&self, file_size: u64) -> bool {
file_size >= self.get_large_document_threshold()
}
/// 获取指定用途的文件大小限制返回FileSize结构体
pub fn get_file_size_limit(&self, _purpose: &FileSizePurpose) -> &FileSize {
// 统一使用同一个文件大小限制
&self.max_file_size
}
}
/// 文件大小用途枚举
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileSizePurpose {
/// 默认用途
Default,
/// 文档解析器
DocumentParser,
/// MinerU解析器
MinerU,
/// MarkItDown解析器
MarkItDown,
/// 图片处理器
ImageProcessor,
/// 文件上传
Upload,
/// 格式检测器
FormatDetector,
/// 内容验证
ContentValidation,
/// 缓存
Cache,
}
/// 解析文件大小字符串 (例如: "100MB", "1GB", "500KB")
pub fn parse_file_size(size_str: &str) -> Result<u64, String> {
let size_str = size_str.trim().to_uppercase();
if let Some(pos) = size_str.find(|c: char| c.is_alphabetic()) {
let (number_part, unit_part) = size_str.split_at(pos);
let number: f64 = number_part
.parse()
.map_err(|_| format!("无效的数字: {number_part}"))?;
let multiplier = match unit_part {
"B" => 1,
"KB" => 1024,
"MB" => 1024 * 1024,
"GB" => 1024 * 1024 * 1024,
"TB" => 1024_u64.pow(4),
_ => return Err(format!("不支持的单位: {unit_part}")),
};
Ok((number * multiplier as f64) as u64)
} else {
// 如果没有单位,假设是字节
size_str
.parse::<u64>()
.map_err(|_| format!("无效的文件大小: {size_str}"))
}
}
/// 配置构建器,用于测试和灵活配置创建
#[derive(Debug, Default)]
pub struct ConfigBuilder {
environment: Option<String>,
server: Option<ServerConfig>,
log: Option<LogConfig>,
document_parser: Option<DocumentParserConfig>,
mineru: Option<MinerUConfig>,
markitdown: Option<MarkItDownConfig>,
storage: Option<StorageConfig>,
external_integration: Option<ExternalIntegrationConfig>,
file_size_config: Option<GlobalFileSizeConfig>,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn environment(mut self, environment: String) -> Self {
self.environment = Some(environment);
self
}
pub fn server(mut self, server: ServerConfig) -> Self {
self.server = Some(server);
self
}
pub fn log(mut self, log: LogConfig) -> Self {
self.log = Some(log);
self
}
pub fn document_parser(mut self, document_parser: DocumentParserConfig) -> Self {
self.document_parser = Some(document_parser);
self
}
pub fn mineru(mut self, mineru: MinerUConfig) -> Self {
self.mineru = Some(mineru);
self
}
pub fn markitdown(mut self, markitdown: MarkItDownConfig) -> Self {
self.markitdown = Some(markitdown);
self
}
pub fn storage(mut self, storage: StorageConfig) -> Self {
self.storage = Some(storage);
self
}
pub fn external_integration(mut self, external_integration: ExternalIntegrationConfig) -> Self {
self.external_integration = Some(external_integration);
self
}
pub fn file_size_config(mut self, file_size_config: GlobalFileSizeConfig) -> Self {
self.file_size_config = Some(file_size_config);
self
}
pub fn build(self) -> Result<AppConfig, ConfigError> {
// 使用默认配置作为基础
let mut config: AppConfig = serde_yaml::from_str(DEFAULT_CONFIG_YAML)
.map_err(|e| ConfigError::Parse(e.to_string()))?;
// 应用构建器中的配置
if let Some(environment) = self.environment {
config.environment = environment;
}
if let Some(server) = self.server {
config.server = server;
}
if let Some(log) = self.log {
config.log = log;
}
if let Some(document_parser) = self.document_parser {
config.document_parser = document_parser;
}
if let Some(mineru) = self.mineru {
config.mineru = mineru;
}
if let Some(markitdown) = self.markitdown {
config.markitdown = markitdown;
}
if let Some(storage) = self.storage {
config.storage = storage;
}
if let Some(external_integration) = self.external_integration {
config.external_integration = external_integration;
}
if let Some(file_size_config) = self.file_size_config {
config.file_size_config = file_size_config;
}
// 验证配置
config.validate()?;
Ok(config)
}
}
/// CUDA环境状态
#[derive(Debug, Clone, Default)]
pub struct CudaStatus {
pub available: bool,
pub version: Option<String>,
pub device_count: usize,
pub recommended_device: Option<String>,
}
/// 应用配置
#[derive(Debug, Clone, Deserialize)]
pub struct AppConfig {
pub environment: String,
pub server: ServerConfig,
pub log: LogConfig,
pub document_parser: DocumentParserConfig,
pub mineru: MinerUConfig,
pub markitdown: MarkItDownConfig,
pub storage: StorageConfig,
pub external_integration: ExternalIntegrationConfig,
/// 全局文件大小配置
#[serde(default)]
pub file_size_config: GlobalFileSizeConfig,
}
/// 服务器配置
#[derive(Debug, Clone, Deserialize)]
pub struct ServerConfig {
pub port: u16,
pub host: String,
}
impl ServerConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
if self.port == 0 {
return Err(ConfigError::Validation {
field: "server.port".to_string(),
message: "端口号不能为0".to_string(),
});
}
if self.host.is_empty() {
return Err(ConfigError::Validation {
field: "server.host".to_string(),
message: "主机地址不能为空".to_string(),
});
}
// 验证主机地址格式
if !self
.host
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == ':' || c == '-')
{
return Err(ConfigError::Validation {
field: "server.host".to_string(),
message: "主机地址包含无效字符".to_string(),
});
}
Ok(())
}
}
/// 日志配置
#[derive(Debug, Clone, Deserialize)]
pub struct LogConfig {
pub level: String,
pub path: String,
/// The number of log files to retain (default: 20)
#[serde(default = "default_retain_days")]
pub retain_days: u32,
}
/// Default log files to retain
fn default_retain_days() -> u32 {
20
}
impl LogConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
// 验证日志级别
let valid_levels = ["trace", "debug", "info", "warn", "error"];
if !valid_levels.contains(&self.level.to_lowercase().as_str()) {
return Err(ConfigError::Validation {
field: "log.level".to_string(),
message: format!(
"无效的日志级别: {},支持的级别: {:?}",
self.level, valid_levels
),
});
}
if self.path.is_empty() {
return Err(ConfigError::Validation {
field: "log.path".to_string(),
message: "日志路径不能为空".to_string(),
});
}
// 验证路径是否可以创建
let path = Path::new(&self.path);
if let Some(parent) = path.parent() {
if parent.exists() && !parent.is_dir() {
return Err(ConfigError::InvalidPath {
path: self.path.clone(),
message: "父目录不是一个有效的目录".to_string(),
});
}
}
Ok(())
}
}
/// 文档解析配置
#[derive(Debug, Clone, Deserialize)]
pub struct DocumentParserConfig {
pub max_concurrent: usize,
pub queue_size: usize,
pub download_timeout: u32,
pub processing_timeout: u32,
}
impl DocumentParserConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
if self.max_concurrent == 0 {
return Err(ConfigError::Validation {
field: "document_parser.max_concurrent".to_string(),
message: "最大并发数不能为0".to_string(),
});
}
if self.max_concurrent > 100 {
return Err(ConfigError::Validation {
field: "document_parser.max_concurrent".to_string(),
message: "最大并发数不能超过100".to_string(),
});
}
if self.queue_size == 0 {
return Err(ConfigError::Validation {
field: "document_parser.queue_size".to_string(),
message: "队列大小不能为0".to_string(),
});
}
// 文件大小限制现在由全局配置管理
if self.download_timeout == 0 {
return Err(ConfigError::Validation {
field: "document_parser.download_timeout".to_string(),
message: "下载超时时间不能为0".to_string(),
});
}
if self.processing_timeout == 0 {
return Err(ConfigError::Validation {
field: "document_parser.processing_timeout".to_string(),
message: "处理超时时间不能为0".to_string(),
});
}
Ok(())
}
}
/// MinerU配置
#[derive(Debug, Clone, Deserialize)]
pub struct MinerUConfig {
#[serde(default = "default_backend")]
pub backend: String,
#[serde(default = "default_python_path")]
pub python_path: String,
pub max_concurrent: usize,
pub queue_size: usize,
#[serde(default)]
pub timeout: u32, // 0表示使用统一的processing_timeout
#[serde(default = "default_batch_size")]
pub batch_size: usize,
#[serde(default)]
pub quality_level: QualityLevel,
#[serde(default = "default_device")]
pub device: String, // 推理设备cpu/cuda/cuda:0/npu/mps等
#[serde(default = "default_vram")]
pub vram: u32, // 单进程最大GPU显存占用(GB)仅对pipeline后端且支持CUDA时有效
}
/// 质量级别
#[derive(Debug, Clone, PartialEq, Deserialize, Default)]
pub enum QualityLevel {
Fast,
#[default]
Balanced,
HighQuality,
}
fn default_batch_size() -> usize {
1
}
fn default_backend() -> String {
"pipeline".to_string()
}
fn default_python_path() -> String {
// Default to virtual environment python if available, otherwise system python
if cfg!(windows) {
"./venv/Scripts/python.exe".to_string()
} else {
"./venv/bin/python".to_string()
}
}
fn default_device() -> String {
"cpu".to_string()
}
fn default_vram() -> u32 {
8 // 默认8GB显存限制
}
impl MinerUConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
let valid_backends = [
"pipeline",
"vlm-transformers",
"vlm-sglang-engine",
"vlm-sglang-client",
];
if !valid_backends.contains(&self.backend.as_str()) {
return Err(ConfigError::Validation {
field: "mineru.backend".to_string(),
message: format!(
"无效的后端类型: {},支持的类型: {:?}",
self.backend, valid_backends
),
});
}
if self.python_path.is_empty() {
return Err(ConfigError::Validation {
field: "mineru.python_path".to_string(),
message: "Python路径不能为空".to_string(),
});
}
if self.max_concurrent == 0 {
return Err(ConfigError::Validation {
field: "mineru.max_concurrent".to_string(),
message: "最大并发数不能为0".to_string(),
});
}
if self.queue_size == 0 {
return Err(ConfigError::Validation {
field: "mineru.queue_size".to_string(),
message: "队列大小不能为0".to_string(),
});
}
// timeout 为 0 表示使用统一的 processing_timeout这是允许的
if self.batch_size == 0 {
return Err(ConfigError::Validation {
field: "mineru.batch_size".to_string(),
message: "批处理大小不能为0".to_string(),
});
}
Ok(())
}
/// Get the effective python path, auto-detecting virtual environment if needed
pub fn get_effective_python_path(&self) -> String {
// If the configured path is the default and a virtual environment exists, use it
let default_path = default_python_path();
if self.python_path == default_path
|| self.python_path == "python3"
|| self.python_path == "python"
{
let venv_python = if cfg!(windows) {
std::path::Path::new("./venv/Scripts/python.exe")
} else {
std::path::Path::new("./venv/bin/python")
};
if venv_python.exists() {
return venv_python.to_string_lossy().to_string();
}
}
self.python_path.clone()
}
}
/// MarkItDown配置
#[derive(Debug, Clone, Deserialize)]
pub struct MarkItDownConfig {
#[serde(default = "default_python_path")]
pub python_path: String,
#[serde(default)]
pub timeout: u32, // 0表示使用统一的processing_timeout
pub enable_plugins: bool,
pub features: MarkItDownFeatures,
}
impl MarkItDownConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
if self.python_path.is_empty() {
return Err(ConfigError::Validation {
field: "markitdown.python_path".to_string(),
message: "Python路径不能为空".to_string(),
});
}
// timeout 为 0 表示使用统一的 processing_timeout这是允许的
Ok(())
}
/// Get the effective python path, auto-detecting virtual environment if needed
pub fn get_effective_python_path(&self) -> String {
// If the configured path is the default and a virtual environment exists, use it
let default_path = default_python_path();
if self.python_path == default_path
|| self.python_path == "python3"
|| self.python_path == "python"
{
let venv_python = if cfg!(windows) {
std::path::Path::new("./venv/Scripts/python.exe")
} else {
std::path::Path::new("./venv/bin/python")
};
if venv_python.exists() {
return venv_python.to_string_lossy().to_string();
}
}
self.python_path.clone()
}
}
/// MarkItDown功能配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarkItDownFeatures {
pub ocr: bool,
pub audio_transcription: bool,
pub azure_doc_intel: bool,
pub youtube_transcription: bool,
}
/// 存储配置
#[derive(Debug, Clone, Deserialize)]
pub struct StorageConfig {
pub sled: SledConfig,
pub oss: OssConfig,
}
impl StorageConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
self.sled.validate()?;
self.oss.validate()?;
Ok(())
}
}
/// Sled数据库配置
#[derive(Debug, Clone, Deserialize)]
pub struct SledConfig {
pub path: String,
pub cache_capacity: usize,
}
impl SledConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
if self.path.is_empty() {
return Err(ConfigError::Validation {
field: "storage.sled.path".to_string(),
message: "数据库路径不能为空".to_string(),
});
}
if self.cache_capacity == 0 {
return Err(ConfigError::Validation {
field: "storage.sled.cache_capacity".to_string(),
message: "缓存容量不能为0".to_string(),
});
}
Ok(())
}
}
/// OSS配置
#[derive(Debug, Clone, Deserialize)]
pub struct OssConfig {
pub endpoint: String,
// pub bucket: String,
/// 公有存储桶名称 (默认: nuwa-packages)
pub public_bucket: String,
/// 私有存储桶名称 (默认: edu-nuwa-packages)
pub private_bucket: String,
pub access_key_id: String,
pub access_key_secret: String,
/// 上传文件的统一子目录前缀
#[serde(default = "default_upload_directory")]
pub upload_directory: String,
/// 区域 (默认: oss-rg-china-mainland)
pub region: String,
}
/// 默认上传目录
fn default_upload_directory() -> String {
"edu".to_string()
}
impl OssConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
if self.endpoint.is_empty() {
return Err(ConfigError::Validation {
field: "storage.oss.endpoint".to_string(),
message: "OSS端点不能为空".to_string(),
});
}
if self.public_bucket.is_empty() {
return Err(ConfigError::Validation {
field: "storage.oss.public_bucket".to_string(),
message: "OSS存储桶名称不能为空".to_string(),
});
}
if self.private_bucket.is_empty() {
return Err(ConfigError::Validation {
field: "storage.oss.private_bucket".to_string(),
message: "OSS存储桶名称不能为空".to_string(),
});
}
// 注意region 现在是可选的,可以为 None
// 注意access_key_id 和 access_key_secret 可以为空,因为它们可能通过环境变量设置
Ok(())
}
/// 检查OSS配置是否完整环境变量是否已设置
pub fn is_configured(&self) -> bool {
!self.access_key_id.is_empty() && !self.access_key_secret.is_empty()
}
}
/// 外部集成配置
#[derive(Debug, Clone, Deserialize)]
pub struct ExternalIntegrationConfig {
pub webhook_url: String,
pub api_key: String,
pub timeout: u32,
}
impl ExternalIntegrationConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
if self.timeout == 0 {
return Err(ConfigError::Validation {
field: "external_integration.timeout".to_string(),
message: "超时时间不能为0".to_string(),
});
}
// webhook_url 和 api_key 可以为空,因为外部集成是可选的
if !self.webhook_url.is_empty() {
// 简单的URL格式验证
if !self.webhook_url.starts_with("http://") && !self.webhook_url.starts_with("https://")
{
return Err(ConfigError::Validation {
field: "external_integration.webhook_url".to_string(),
message: "Webhook URL必须以http://或https://开头".to_string(),
});
}
}
Ok(())
}
}
impl AppConfig {
/// 加载配置文件,支持多种配置源和环境变量覆盖
pub fn load_config() -> Result<Self, ConfigError> {
// 1. 首先加载基础配置
let mut config = Self::load_base_config()?;
// 2. 从环境变量覆盖配置
config.load_all_from_env()?;
// 3. 验证最终配置
config.validate()?;
// 4. 初始化必要的目录
config.initialize_directories()?;
Ok(config)
}
/// 加载基础配置文件
pub fn load_base_config() -> Result<Self, ConfigError> {
Self::load_base_config_with_path(None)
}
/// 加载基础配置文件,支持可选的配置文件路径
pub fn load_base_config_with_path(config_path: Option<String>) -> Result<Self, ConfigError> {
// 优先尝试从传入的路径加载
if let Some(path) = config_path {
if Path::new(&path).exists() {
return Self::load_from_file(&path);
}
}
let config_paths = [
"/app/config.yml",
"config.yml",
"document-parser/config.yml",
];
// 尝试从环境变量指定的配置文件路径加载
if let Ok(env_config_path) = env::var("DOCUMENT_PARSER_CONFIG") {
return Self::load_from_file(&env_config_path);
}
// 尝试从预定义路径加载
for path in &config_paths {
if Path::new(path).exists() {
return Self::load_from_file(path);
}
}
// 如果都没有找到,尝试在当前目录创建默认配置文件
if let Err(e) = Self::create_default_config_in_current_dir() {
warn!(
"Unable to create default configuration file in current directory: {}",
e
);
}
// 使用默认配置
serde_yaml::from_str::<AppConfig>(DEFAULT_CONFIG_YAML)
.map_err(|e| ConfigError::Parse(format!("解析默认配置失败: {e}")))
}
/// 从指定文件加载配置
fn load_from_file(path: &str) -> Result<Self, ConfigError> {
let file = File::open(path)
.map_err(|e| ConfigError::FileRead(format!("无法打开配置文件 {path}: {e}")))?;
serde_yaml::from_reader(file)
.map_err(|e| ConfigError::Parse(format!("解析配置文件 {path} 失败: {e}")))
}
/// 在当前目录创建默认配置文件
fn create_default_config_in_current_dir() -> Result<(), ConfigError> {
let current_dir = std::env::current_dir()
.map_err(|e| ConfigError::FileRead(format!("无法获取当前目录: {e}")))?;
let config_path = current_dir.join("config.yml");
// 如果配置文件已存在,不覆盖
if config_path.exists() {
return Ok(());
}
// 创建配置文件
std::fs::write(&config_path, DEFAULT_CONFIG_YAML)
.map_err(|e| ConfigError::FileRead(format!("无法创建默认配置文件: {e}")))?;
info!(
"The default configuration file has been created in the current directory: {}",
config_path.display()
);
Ok(())
}
/// 验证整个配置的有效性
pub fn validate(&self) -> Result<(), ConfigError> {
self.server.validate()?;
self.log.validate()?;
self.document_parser.validate()?;
self.mineru.validate()?;
self.markitdown.validate()?;
self.storage.validate()?;
self.external_integration.validate()?;
self.file_size_config.validate()?;
// 交叉验证
self.cross_validate()?;
Ok(())
}
/// 交叉验证不同配置项之间的一致性
fn cross_validate(&self) -> Result<(), ConfigError> {
// 验证并发配置的一致性
if self.document_parser.max_concurrent < self.mineru.max_concurrent + 1 {
return Err(ConfigError::Validation {
field: "document_parser.max_concurrent".to_string(),
message: "文档解析器的最大并发数应该大于MinerU的最大并发数".to_string(),
});
}
// 验证队列大小的合理性
if self.document_parser.queue_size < self.mineru.queue_size {
return Err(ConfigError::Validation {
field: "document_parser.queue_size".to_string(),
message: "文档解析器的队列大小应该大于等于MinerU的队列大小".to_string(),
});
}
// 验证超时配置的合理性0表示使用统一超时跳过验证
if self.mineru.timeout > 0 && self.document_parser.processing_timeout < self.mineru.timeout
{
return Err(ConfigError::Validation {
field: "document_parser.processing_timeout".to_string(),
message: "文档解析器的处理超时时间应该大于等于MinerU的超时时间".to_string(),
});
}
if self.markitdown.timeout > 0
&& self.document_parser.processing_timeout < self.markitdown.timeout
{
return Err(ConfigError::Validation {
field: "document_parser.processing_timeout".to_string(),
message: "文档解析器的处理超时时间应该大于等于MarkItDown的超时时间".to_string(),
});
}
Ok(())
}
/// 初始化必要的目录
pub fn initialize_directories(&self) -> Result<(), ConfigError> {
let directories = [&self.log.path, &self.storage.sled.path];
for dir_path in &directories {
let path = Path::new(dir_path);
// 如果是文件路径,获取父目录
let dir_to_create = if path.extension().is_some() {
path.parent().unwrap_or(path)
} else {
path
};
if !dir_to_create.exists() {
std::fs::create_dir_all(dir_to_create).map_err(|e| ConfigError::InvalidPath {
path: dir_to_create.to_string_lossy().to_string(),
message: format!("无法创建目录: {e}"),
})?;
}
}
Ok(())
}
/// 从环境变量加载所有配置,支持类型安全的解析和错误处理
pub fn load_all_from_env(&mut self) -> Result<(), ConfigError> {
self.load_server_config_from_env()?;
self.load_log_config_from_env()?;
self.load_document_parser_config_from_env()?;
self.load_oss_config_from_env()?;
self.load_mineru_config_from_env()?;
self.load_markitdown_config_from_env()?;
self.load_external_integration_config_from_env()?;
Ok(())
}
/// 从环境变量加载服务器配置
fn load_server_config_from_env(&mut self) -> Result<(), ConfigError> {
if let Ok(port_str) = env::var("SERVER_PORT") {
self.server.port = Self::parse_env_var("SERVER_PORT", &port_str)?;
}
if let Ok(host) = env::var("SERVER_HOST") {
self.server.host = host;
}
Ok(())
}
/// 从环境变量加载日志配置
fn load_log_config_from_env(&mut self) -> Result<(), ConfigError> {
if let Ok(level) = env::var("LOG_LEVEL") {
self.log.level = level;
}
if let Ok(path) = env::var("LOG_PATH") {
self.log.path = path;
}
Ok(())
}
/// 从环境变量加载文档解析器配置
fn load_document_parser_config_from_env(&mut self) -> Result<(), ConfigError> {
if let Ok(max_concurrent_str) = env::var("DOCUMENT_PARSER_MAX_CONCURRENT") {
self.document_parser.max_concurrent =
Self::parse_env_var("DOCUMENT_PARSER_MAX_CONCURRENT", &max_concurrent_str)?;
}
if let Ok(queue_size_str) = env::var("DOCUMENT_PARSER_QUEUE_SIZE") {
self.document_parser.queue_size =
Self::parse_env_var("DOCUMENT_PARSER_QUEUE_SIZE", &queue_size_str)?;
}
// 文件大小限制现在由全局配置管理
if let Ok(download_timeout_str) = env::var("DOCUMENT_PARSER_DOWNLOAD_TIMEOUT") {
self.document_parser.download_timeout =
Self::parse_env_var("DOCUMENT_PARSER_DOWNLOAD_TIMEOUT", &download_timeout_str)?;
}
if let Ok(processing_timeout_str) = env::var("DOCUMENT_PARSER_PROCESSING_TIMEOUT") {
self.document_parser.processing_timeout = Self::parse_env_var(
"DOCUMENT_PARSER_PROCESSING_TIMEOUT",
&processing_timeout_str,
)?;
}
Ok(())
}
/// 从环境变量加载OSS配置
fn load_oss_config_from_env(&mut self) -> Result<(), ConfigError> {
if let Ok(endpoint) = env::var("ALIYUN_OSS_ENDPOINT") {
self.storage.oss.endpoint = endpoint;
}
if let Ok(public_bucket) = env::var("ALIYUN_OSS_PUBLIC_BUCKET") {
self.storage.oss.public_bucket = public_bucket;
}
if let Ok(private_bucket) = env::var("ALIYUN_OSS_PRIVATE_BUCKET") {
self.storage.oss.private_bucket = private_bucket;
}
if let Ok(access_key_id) = env::var("OSS_ACCESS_KEY_ID") {
self.storage.oss.access_key_id = access_key_id;
}
if let Ok(access_key_secret) = env::var("OSS_ACCESS_KEY_SECRET") {
self.storage.oss.access_key_secret = access_key_secret;
}
Ok(())
}
/// 从环境变量加载MinerU配置
fn load_mineru_config_from_env(&mut self) -> Result<(), ConfigError> {
if let Ok(backend) = env::var("MINERU_BACKEND") {
self.mineru.backend = backend;
}
if let Ok(python_path) = env::var("MINERU_PYTHON_PATH") {
self.mineru.python_path = python_path;
}
if let Ok(max_concurrent_str) = env::var("MINERU_MAX_CONCURRENT") {
self.mineru.max_concurrent =
Self::parse_env_var("MINERU_MAX_CONCURRENT", &max_concurrent_str)?;
}
if let Ok(queue_size_str) = env::var("MINERU_QUEUE_SIZE") {
self.mineru.queue_size = Self::parse_env_var("MINERU_QUEUE_SIZE", &queue_size_str)?;
}
if let Ok(timeout_str) = env::var("MINERU_TIMEOUT") {
self.mineru.timeout = Self::parse_env_var("MINERU_TIMEOUT", &timeout_str)?;
}
if let Ok(batch_size_str) = env::var("MINERU_BATCH_SIZE") {
self.mineru.batch_size = Self::parse_env_var("MINERU_BATCH_SIZE", &batch_size_str)?;
}
if let Ok(device) = env::var("MINERU_DEVICE") {
self.mineru.device = device;
}
Ok(())
}
/// 从环境变量加载MarkItDown配置
fn load_markitdown_config_from_env(&mut self) -> Result<(), ConfigError> {
if let Ok(python_path) = env::var("MARKITDOWN_PYTHON_PATH") {
self.markitdown.python_path = python_path;
}
if let Ok(timeout_str) = env::var("MARKITDOWN_TIMEOUT") {
self.markitdown.timeout = Self::parse_env_var("MARKITDOWN_TIMEOUT", &timeout_str)?;
}
if let Ok(enable_plugins_str) = env::var("MARKITDOWN_ENABLE_PLUGINS") {
self.markitdown.enable_plugins =
Self::parse_env_var("MARKITDOWN_ENABLE_PLUGINS", &enable_plugins_str)?;
}
if let Ok(enable_ocr_str) = env::var("MARKITDOWN_ENABLE_OCR") {
self.markitdown.features.ocr =
Self::parse_env_var("MARKITDOWN_ENABLE_OCR", &enable_ocr_str)?;
}
if let Ok(enable_audio_transcription_str) =
env::var("MARKITDOWN_ENABLE_AUDIO_TRANSCRIPTION")
{
self.markitdown.features.audio_transcription = Self::parse_env_var(
"MARKITDOWN_ENABLE_AUDIO_TRANSCRIPTION",
&enable_audio_transcription_str,
)?;
}
if let Ok(enable_azure_doc_intel_str) = env::var("MARKITDOWN_ENABLE_AZURE_DOC_INTEL") {
self.markitdown.features.azure_doc_intel = Self::parse_env_var(
"MARKITDOWN_ENABLE_AZURE_DOC_INTEL",
&enable_azure_doc_intel_str,
)?;
}
if let Ok(enable_youtube_transcription_str) =
env::var("MARKITDOWN_ENABLE_YOUTUBE_TRANSCRIPTION")
{
self.markitdown.features.youtube_transcription = Self::parse_env_var(
"MARKITDOWN_ENABLE_YOUTUBE_TRANSCRIPTION",
&enable_youtube_transcription_str,
)?;
}
Ok(())
}
/// 从环境变量加载外部集成配置
fn load_external_integration_config_from_env(&mut self) -> Result<(), ConfigError> {
if let Ok(webhook_url) = env::var("EXTERNAL_INTEGRATION_WEBHOOK_URL") {
self.external_integration.webhook_url = webhook_url;
}
if let Ok(api_key) = env::var("EXTERNAL_INTEGRATION_API_KEY") {
self.external_integration.api_key = api_key;
}
if let Ok(timeout_str) = env::var("EXTERNAL_INTEGRATION_TIMEOUT") {
self.external_integration.timeout =
Self::parse_env_var("EXTERNAL_INTEGRATION_TIMEOUT", &timeout_str)?;
}
Ok(())
}
/// 类型安全的环境变量解析
fn parse_env_var<T>(var_name: &str, value: &str) -> Result<T, ConfigError>
where
T: FromStr,
T::Err: std::fmt::Display,
{
value.parse::<T>().map_err(|e| ConfigError::EnvVar {
var: var_name.to_string(),
message: format!("无法解析值 '{value}': {e}"),
})
}
/// 获取配置构建器,用于测试
pub fn builder() -> ConfigBuilder {
ConfigBuilder::new()
}
/// 生成配置摘要,用于日志记录(隐藏敏感信息)
pub fn summary(&self) -> String {
format!(
"AppConfig {{ server: {}:{}, log: {}, max_concurrent: {}, storage: sled={}, oss={}:{} }}",
self.server.host,
self.server.port,
self.log.level,
self.document_parser.max_concurrent,
self.storage.sled.path,
self.storage.oss.endpoint,
self.storage.oss.public_bucket
)
}
}
/// 全局配置实例
static GLOBAL_CONFIG: OnceLock<AppConfig> = OnceLock::new();
/// 初始化全局配置
pub fn init_global_config(config: AppConfig) -> Result<(), ConfigError> {
// 检查是否已经初始化
if GLOBAL_CONFIG.get().is_some() {
return Ok(()); // 已经初始化过了,直接返回成功
}
GLOBAL_CONFIG
.set(config)
.map_err(|_| ConfigError::Validation {
field: "global_config".to_string(),
message: "全局配置已经初始化过了".to_string(),
})?;
Ok(())
}
/// 获取全局配置引用
pub fn get_global_config() -> &'static AppConfig {
GLOBAL_CONFIG
.get()
.expect("全局配置尚未初始化,请先调用 init_global_config")
}
/// 获取全局文件大小配置
pub fn get_global_file_size_config() -> &'static GlobalFileSizeConfig {
let max_file_sieze = &get_global_config().file_size_config;
info!("Global file size configuration: {:?}", max_file_sieze);
max_file_sieze
}
/// 便捷函数:获取指定用途的文件大小限制
pub fn get_file_size_limit(purpose: &FileSizePurpose) -> &'static FileSize {
get_global_file_size_config().get_file_size_limit(purpose)
}
/// 便捷函数:检查文件大小是否允许
pub fn is_file_size_allowed(file_size: u64, purpose: FileSizePurpose) -> bool {
get_global_file_size_config().is_size_allowed(file_size, purpose)
}
/// 便捷函数:检查是否为大文档
pub fn is_large_document(file_size: u64) -> bool {
get_global_file_size_config().is_large_document(file_size)
}
/// 便捷函数:获取大文档阈值
pub fn get_large_document_threshold() -> u64 {
get_global_file_size_config().get_large_document_threshold()
}
/// 便捷函数检查CUDA是否可用
pub fn is_cuda_available() -> bool {
get_global_cuda_status_clone().available
}
/// 便捷函数获取推荐的CUDA设备
pub fn get_recommended_cuda_device() -> Option<String> {
get_global_cuda_status_clone().recommended_device
}
/// 全局CUDA状态管理使用线程安全的Arc<RwLock<>>
static GLOBAL_CUDA_STATUS: OnceLock<Arc<RwLock<CudaStatus>>> = OnceLock::new();
/// 初始化全局CUDA状态
pub fn init_global_cuda_status(cuda_status: CudaStatus) -> Result<(), ConfigError> {
GLOBAL_CUDA_STATUS
.set(Arc::new(RwLock::new(cuda_status)))
.map_err(|_| ConfigError::Validation {
field: "global_cuda_status".to_string(),
message: "全局CUDA状态已经初始化过了".to_string(),
})?;
Ok(())
}
/// 更新全局CUDA状态
pub fn update_global_cuda_status(cuda_status: CudaStatus) -> Result<(), ConfigError> {
if let Some(global_status) = GLOBAL_CUDA_STATUS.get() {
let mut status = global_status.write().map_err(|_| ConfigError::Validation {
field: "global_cuda_status".to_string(),
message: "无法获取CUDA状态写锁".to_string(),
})?;
*status = cuda_status;
Ok(())
} else {
Err(ConfigError::Validation {
field: "global_cuda_status".to_string(),
message: "全局CUDA状态尚未初始化".to_string(),
})
}
}
/// 获取全局CUDA状态的克隆
pub fn get_global_cuda_status_clone() -> CudaStatus {
if let Some(global_status) = GLOBAL_CUDA_STATUS.get() {
if let Ok(status) = global_status.read() {
return status.clone();
}
}
warn!("The global CUDA state has not been initialized and returns to the default value.");
CudaStatus::default()
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use tempfile::TempDir;
#[test]
fn test_file_size_parsing() {
assert_eq!(parse_file_size("100B").unwrap(), 100);
assert_eq!(parse_file_size("1KB").unwrap(), 1024);
assert_eq!(parse_file_size("1MB").unwrap(), 1024 * 1024);
assert_eq!(parse_file_size("1GB").unwrap(), 1024 * 1024 * 1024);
assert_eq!(
parse_file_size("2.5MB").unwrap(),
(2.5 * 1024.0 * 1024.0) as u64
);
// 测试无效格式
assert!(parse_file_size("invalid").is_err());
assert!(parse_file_size("100XB").is_err());
assert!(parse_file_size("").is_err());
}
#[test]
fn test_default_config_loading() {
let config = AppConfig::load_base_config().unwrap();
// 验证默认值
assert_eq!(config.server.port, 8087);
assert_eq!(config.server.host, "0.0.0.0");
assert_eq!(config.log.level, "info");
assert_eq!(config.document_parser.max_concurrent, 5); // 配置文件中的实际值
}
#[test]
fn test_config_validation() {
let mut config = AppConfig::load_base_config().unwrap();
// 测试有效配置
assert!(config.validate().is_ok());
// 测试无效端口
config.server.port = 0;
assert!(config.validate().is_err());
// 恢复有效端口,测试无效日志级别
config.server.port = 8087;
config.log.level = "invalid".to_string();
assert!(config.validate().is_err());
}
#[test]
fn test_config_builder() {
let server_config = ServerConfig {
port: 9000,
host: "127.0.0.1".to_string(),
};
let config = ConfigBuilder::new().server(server_config).build().unwrap();
assert_eq!(config.server.port, 9000);
assert_eq!(config.server.host, "127.0.0.1");
}
#[test]
#[ignore = "Modifies global environment variables, causes race conditions with parallel tests"]
fn test_environment_variable_override() {
// 设置环境变量
unsafe {
env::set_var("SERVER_PORT", "9999");
env::set_var("LOG_LEVEL", "debug");
}
let mut config = AppConfig::load_base_config().unwrap();
config.load_all_from_env().unwrap();
assert_eq!(config.server.port, 9999);
assert_eq!(config.log.level, "debug");
// 清理环境变量
unsafe {
env::remove_var("SERVER_PORT");
env::remove_var("LOG_LEVEL");
}
}
#[test]
#[ignore = "Modifies global environment variables, causes race conditions with parallel tests"]
fn test_invalid_environment_variables() {
// 设置无效的环境变量
unsafe {
env::set_var("SERVER_PORT", "invalid_port");
}
let mut config = AppConfig::load_base_config().unwrap();
let result = config.load_all_from_env();
assert!(result.is_err());
// 清理环境变量
unsafe {
env::remove_var("SERVER_PORT");
}
}
#[test]
fn test_cross_validation() {
let mut config = AppConfig::load_base_config().unwrap();
// 设置不一致的并发配置
config.document_parser.max_concurrent = 1;
config.mineru.max_concurrent = 5;
assert!(config.validate().is_err());
}
#[test]
fn test_directory_initialization() {
let temp_dir = TempDir::new().unwrap();
let temp_path = temp_dir.path().to_string_lossy().to_string();
let mut config = AppConfig::load_base_config().unwrap();
config.log.path = format!("{temp_path}/logs/app.log");
// temp_dir is now hardcoded, no need to set it
config.storage.sled.path = format!("{temp_path}/sled");
assert!(config.initialize_directories().is_ok());
// 验证目录是否创建
assert!(Path::new(&format!("{temp_path}/logs")).exists());
// 注意initialize_directories 方法不再创建 temp/mineru 和 temp/markitdown
assert!(Path::new(&format!("{temp_path}/sled")).exists());
}
#[test]
fn test_server_config_validation() {
let mut config = ServerConfig {
port: 8080,
host: "localhost".to_string(),
};
assert!(config.validate().is_ok());
// 测试无效端口
config.port = 0;
assert!(config.validate().is_err());
// 测试空主机
config.port = 8080;
config.host = "".to_string();
assert!(config.validate().is_err());
}
#[test]
fn test_log_config_validation() {
let mut config = LogConfig {
level: "info".to_string(),
path: "/tmp/test.log".to_string(),
retain_days: 20,
};
assert!(config.validate().is_ok());
// 测试无效日志级别
config.level = "invalid".to_string();
assert!(config.validate().is_err());
// 测试空路径
config.level = "info".to_string();
config.path = "".to_string();
assert!(config.validate().is_err());
}
#[test]
fn test_document_parser_config_validation() {
let config = DocumentParserConfig {
max_concurrent: 3,
queue_size: 100,
download_timeout: 3600,
processing_timeout: 1800,
};
assert!(config.validate().is_ok());
// 测试零并发
let mut invalid_config = config.clone();
invalid_config.max_concurrent = 0;
assert!(invalid_config.validate().is_err());
// 测试过大的并发数
invalid_config.max_concurrent = 200;
assert!(invalid_config.validate().is_err());
}
#[test]
fn test_mineru_config_validation() {
let config = MinerUConfig {
backend: "pipeline".to_string(),
python_path: "/usr/bin/python3".to_string(),
max_concurrent: 3,
queue_size: 100,
timeout: 0, // 使用统一超时配置
batch_size: 10,
quality_level: QualityLevel::Balanced,
device: "cpu".to_string(),
vram: 8, // 默认显存限制
};
assert!(config.validate().is_ok());
// 测试无效后端
let mut invalid_config = config.clone();
invalid_config.backend = "invalid".to_string();
assert!(invalid_config.validate().is_err());
}
#[test]
fn test_external_integration_config_validation() {
let config = ExternalIntegrationConfig {
webhook_url: "https://example.com/webhook".to_string(),
api_key: "test-key".to_string(),
timeout: 30,
};
assert!(config.validate().is_ok());
// 测试无效URL
let mut invalid_config = config.clone();
invalid_config.webhook_url = "invalid-url".to_string();
assert!(invalid_config.validate().is_err());
// 测试零超时
invalid_config.webhook_url = "https://example.com/webhook".to_string();
invalid_config.timeout = 0;
assert!(invalid_config.validate().is_err());
}
#[test]
fn test_config_summary() {
let config = AppConfig::load_base_config().unwrap();
let summary = config.summary();
assert!(summary.contains("AppConfig"));
assert!(summary.contains("0.0.0.0:8087"));
assert!(summary.contains("info"));
assert!(!summary.contains("access_key")); // 确保敏感信息不在摘要中
}
}