2025-09-19 19:32:42 +01:00
|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
|
|
|
|
|
统一配置管理模块
|
2026-02-11 00:08:09 +08:00
|
|
|
|
从环境变量加载所有配置,提供统一的配置接口
|
2025-09-19 19:32:42 +01:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from typing import Dict, Any, Optional
|
|
|
|
|
|
from dataclasses import dataclass, asdict
|
2026-02-11 00:08:09 +08:00
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
|
|
|
|
|
|
# 在模块加载时,自动从.env文件加载环境变量
|
|
|
|
|
|
# 这使得所有后续的os.getenv调用都能获取到.env中定义的值
|
|
|
|
|
|
load_dotenv()
|
2025-09-19 19:32:42 +01:00
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
2026-02-11 00:08:09 +08:00
|
|
|
|
# --- 数据类定义 ---
|
|
|
|
|
|
# 这些类定义了配置的结构,但不包含敏感的默认值。
|
|
|
|
|
|
# 默认值只用于那些不敏感或在大多数环境中都相同的值。
|
|
|
|
|
|
|
2025-09-19 19:32:42 +01:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class DatabaseConfig:
|
|
|
|
|
|
"""数据库配置"""
|
2026-02-11 00:08:09 +08:00
|
|
|
|
url: str
|
2025-09-19 19:32:42 +01:00
|
|
|
|
pool_size: int = 10
|
|
|
|
|
|
max_overflow: int = 20
|
|
|
|
|
|
pool_timeout: int = 30
|
2026-02-11 00:08:09 +08:00
|
|
|
|
pool_recycle: int = 600 # 改为 10 分钟回收连接,避免连接超时
|
2025-09-19 19:32:42 +01:00
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class LLMConfig:
|
|
|
|
|
|
"""LLM配置"""
|
2026-02-11 00:08:09 +08:00
|
|
|
|
provider: str
|
|
|
|
|
|
api_key: str
|
|
|
|
|
|
model: str
|
|
|
|
|
|
base_url: Optional[str] = None
|
2025-09-19 19:32:42 +01:00
|
|
|
|
temperature: float = 0.7
|
|
|
|
|
|
max_tokens: int = 2000
|
|
|
|
|
|
timeout: int = 30
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class ServerConfig:
|
|
|
|
|
|
"""服务器配置"""
|
|
|
|
|
|
host: str = "0.0.0.0"
|
|
|
|
|
|
port: int = 5000
|
|
|
|
|
|
websocket_port: int = 8765
|
|
|
|
|
|
debug: bool = False
|
|
|
|
|
|
log_level: str = "INFO"
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class FeishuConfig:
|
|
|
|
|
|
"""飞书配置"""
|
2026-02-11 00:08:09 +08:00
|
|
|
|
app_id: Optional[str] = None
|
|
|
|
|
|
app_secret: Optional[str] = None
|
2026-02-11 00:29:18 +08:00
|
|
|
|
app_token: Optional[str] = None
|
2026-02-11 00:08:09 +08:00
|
|
|
|
verification_token: Optional[str] = None
|
|
|
|
|
|
encrypt_key: Optional[str] = None
|
|
|
|
|
|
table_id: Optional[str] = None
|
|
|
|
|
|
|
2025-09-19 19:32:42 +01:00
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class AIAccuracyConfig:
|
|
|
|
|
|
"""AI准确率配置"""
|
|
|
|
|
|
auto_approve_threshold: float = 0.95
|
|
|
|
|
|
use_human_resolution_threshold: float = 0.90
|
|
|
|
|
|
manual_review_threshold: float = 0.80
|
|
|
|
|
|
ai_suggestion_confidence: float = 0.95
|
|
|
|
|
|
human_resolution_confidence: float = 0.90
|
|
|
|
|
|
|
2026-02-11 00:08:09 +08:00
|
|
|
|
|
2026-03-20 16:50:26 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class EmbeddingConfig:
|
|
|
|
|
|
"""Embedding 向量配置"""
|
|
|
|
|
|
enabled: bool = True
|
|
|
|
|
|
api_key: Optional[str] = None # 本地模式不需要
|
|
|
|
|
|
base_url: Optional[str] = None # 本地模式不需要
|
|
|
|
|
|
model: str = "BAAI/bge-small-zh-v1.5" # 本地轻量中文模型
|
|
|
|
|
|
dimension: int = 512 # bge-small-zh 输出维度
|
|
|
|
|
|
batch_size: int = 32
|
|
|
|
|
|
similarity_threshold: float = 0.5 # 语义搜索相似度阈值
|
|
|
|
|
|
cache_ttl: int = 86400 # embedding 缓存过期时间(秒),默认 1 天
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-11 14:10:18 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class RedisConfig:
|
|
|
|
|
|
"""Redis缓存配置"""
|
|
|
|
|
|
enabled: bool = True
|
|
|
|
|
|
host: str = "localhost"
|
|
|
|
|
|
port: int = 6379
|
|
|
|
|
|
db: int = 0
|
|
|
|
|
|
password: Optional[str] = None
|
|
|
|
|
|
pool_size: int = 10
|
|
|
|
|
|
default_ttl: int = 3600 # 默认缓存过期时间(秒)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-11 00:08:09 +08:00
|
|
|
|
# --- 统一配置管理器 ---
|
2025-09-19 19:32:42 +01:00
|
|
|
|
|
|
|
|
|
|
class UnifiedConfig:
|
2026-02-11 00:08:09 +08:00
|
|
|
|
"""
|
|
|
|
|
|
统一配置管理器
|
|
|
|
|
|
在实例化时,从环境变量中加载所有配置。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
logger.info("Initializing unified configuration from environment variables...")
|
|
|
|
|
|
self.database = self._load_database_from_env()
|
|
|
|
|
|
self.llm = self._load_llm_from_env()
|
|
|
|
|
|
self.server = self._load_server_from_env()
|
|
|
|
|
|
self.feishu = self._load_feishu_from_env()
|
|
|
|
|
|
self.ai_accuracy = self._load_ai_accuracy_from_env()
|
2026-02-11 14:10:18 +08:00
|
|
|
|
self.redis = self._load_redis_from_env()
|
2026-03-20 16:50:26 +08:00
|
|
|
|
self.embedding = self._load_embedding_from_env()
|
2026-02-11 00:08:09 +08:00
|
|
|
|
self.validate_config()
|
|
|
|
|
|
|
|
|
|
|
|
def _load_database_from_env(self) -> DatabaseConfig:
|
|
|
|
|
|
db_url = os.getenv("DATABASE_URL")
|
|
|
|
|
|
if not db_url:
|
|
|
|
|
|
raise ValueError("FATAL: DATABASE_URL environment variable is not set.")
|
|
|
|
|
|
logger.info("Database config loaded.")
|
|
|
|
|
|
return DatabaseConfig(url=db_url)
|
|
|
|
|
|
|
|
|
|
|
|
def _load_llm_from_env(self) -> LLMConfig:
|
|
|
|
|
|
api_key = os.getenv("LLM_API_KEY")
|
|
|
|
|
|
if not api_key:
|
|
|
|
|
|
logger.warning("LLM_API_KEY is not set. LLM functionality will be disabled or fail.")
|
|
|
|
|
|
|
|
|
|
|
|
config = LLMConfig(
|
|
|
|
|
|
provider=os.getenv("LLM_PROVIDER", "qwen"),
|
|
|
|
|
|
api_key=api_key,
|
|
|
|
|
|
model=os.getenv("LLM_MODEL", "qwen-plus-latest"),
|
|
|
|
|
|
base_url=os.getenv("LLM_BASE_URL"),
|
|
|
|
|
|
temperature=float(os.getenv("LLM_TEMPERATURE", 0.7)),
|
|
|
|
|
|
max_tokens=int(os.getenv("LLM_MAX_TOKENS", 2000)),
|
|
|
|
|
|
timeout=int(os.getenv("LLM_TIMEOUT", 30))
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info("LLM config loaded.")
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
def _load_server_from_env(self) -> ServerConfig:
|
|
|
|
|
|
config = ServerConfig(
|
|
|
|
|
|
host=os.getenv("SERVER_HOST", "0.0.0.0"),
|
|
|
|
|
|
port=int(os.getenv("SERVER_PORT", 5000)),
|
|
|
|
|
|
websocket_port=int(os.getenv("WEBSOCKET_PORT", 8765)),
|
|
|
|
|
|
debug=os.getenv("DEBUG_MODE", "False").lower() in ('true', '1', 't'),
|
|
|
|
|
|
log_level=os.getenv("LOG_LEVEL", "INFO").upper()
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info("Server config loaded.")
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
def _load_feishu_from_env(self) -> FeishuConfig:
|
|
|
|
|
|
config = FeishuConfig(
|
|
|
|
|
|
app_id=os.getenv("FEISHU_APP_ID"),
|
|
|
|
|
|
app_secret=os.getenv("FEISHU_APP_SECRET"),
|
2026-02-11 00:29:18 +08:00
|
|
|
|
app_token=os.getenv("FEISHU_APP_TOKEN"),
|
2026-02-11 00:08:09 +08:00
|
|
|
|
verification_token=os.getenv("FEISHU_VERIFICATION_TOKEN"),
|
|
|
|
|
|
encrypt_key=os.getenv("FEISHU_ENCRYPT_KEY"),
|
|
|
|
|
|
table_id=os.getenv("FEISHU_TABLE_ID")
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info("Feishu config loaded.")
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
def _load_ai_accuracy_from_env(self) -> AIAccuracyConfig:
|
|
|
|
|
|
config = AIAccuracyConfig(
|
|
|
|
|
|
auto_approve_threshold=float(os.getenv("AI_AUTO_APPROVE_THRESHOLD", 0.95)),
|
|
|
|
|
|
use_human_resolution_threshold=float(os.getenv("AI_USE_HUMAN_RESOLUTION_THRESHOLD", 0.90)),
|
|
|
|
|
|
manual_review_threshold=float(os.getenv("AI_MANUAL_REVIEW_THRESHOLD", 0.80)),
|
|
|
|
|
|
ai_suggestion_confidence=float(os.getenv("AI_SUGGESTION_CONFIDENCE", 0.95)),
|
|
|
|
|
|
human_resolution_confidence=float(os.getenv("AI_HUMAN_RESOLUTION_CONFIDENCE", 0.90))
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info("AI Accuracy config loaded.")
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
2026-02-11 14:10:18 +08:00
|
|
|
|
def _load_redis_from_env(self) -> RedisConfig:
|
|
|
|
|
|
config = RedisConfig(
|
|
|
|
|
|
enabled=os.getenv("REDIS_ENABLED", "True").lower() in ('true', '1', 't'),
|
|
|
|
|
|
host=os.getenv("REDIS_HOST", "localhost"),
|
|
|
|
|
|
port=int(os.getenv("REDIS_PORT", 6379)),
|
|
|
|
|
|
db=int(os.getenv("REDIS_DB", 0)),
|
|
|
|
|
|
password=os.getenv("REDIS_PASSWORD") or None,
|
|
|
|
|
|
pool_size=int(os.getenv("REDIS_POOL_SIZE", 10)),
|
|
|
|
|
|
default_ttl=int(os.getenv("REDIS_DEFAULT_TTL", 3600))
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info("Redis config loaded.")
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
2026-03-20 16:50:26 +08:00
|
|
|
|
def _load_embedding_from_env(self) -> EmbeddingConfig:
|
|
|
|
|
|
config = EmbeddingConfig(
|
|
|
|
|
|
enabled=os.getenv("EMBEDDING_ENABLED", "True").lower() in ('true', '1', 't'),
|
|
|
|
|
|
model=os.getenv("EMBEDDING_MODEL", "BAAI/bge-small-zh-v1.5"),
|
|
|
|
|
|
dimension=int(os.getenv("EMBEDDING_DIMENSION", 512)),
|
|
|
|
|
|
batch_size=int(os.getenv("EMBEDDING_BATCH_SIZE", 32)),
|
|
|
|
|
|
similarity_threshold=float(os.getenv("EMBEDDING_SIMILARITY_THRESHOLD", 0.5)),
|
|
|
|
|
|
cache_ttl=int(os.getenv("EMBEDDING_CACHE_TTL", 86400)),
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info("Embedding config loaded.")
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
2026-02-11 00:08:09 +08:00
|
|
|
|
def validate_config(self):
|
|
|
|
|
|
"""在启动时验证关键配置"""
|
|
|
|
|
|
if not self.database.url:
|
|
|
|
|
|
raise ValueError("Database URL is missing.")
|
|
|
|
|
|
if not self.llm.api_key:
|
|
|
|
|
|
logger.warning("LLM API key is not configured. AI features may fail.")
|
|
|
|
|
|
if self.feishu.app_id and not self.feishu.app_secret:
|
|
|
|
|
|
logger.warning("FEISHU_APP_ID is set, but FEISHU_APP_SECRET is missing.")
|
|
|
|
|
|
logger.info("Configuration validation passed (warnings may exist).")
|
|
|
|
|
|
|
|
|
|
|
|
# --- Public Getters ---
|
|
|
|
|
|
|
2025-09-19 19:32:42 +01:00
|
|
|
|
def get_all_config(self) -> Dict[str, Any]:
|
2026-02-11 00:08:09 +08:00
|
|
|
|
"""获取所有配置的字典表示"""
|
2025-09-19 19:32:42 +01:00
|
|
|
|
return {
|
|
|
|
|
|
'database': asdict(self.database),
|
|
|
|
|
|
'llm': asdict(self.llm),
|
|
|
|
|
|
'server': asdict(self.server),
|
|
|
|
|
|
'feishu': asdict(self.feishu),
|
|
|
|
|
|
'ai_accuracy': asdict(self.ai_accuracy),
|
2026-02-11 14:10:18 +08:00
|
|
|
|
'redis': asdict(self.redis),
|
2026-03-20 16:50:26 +08:00
|
|
|
|
'embedding': asdict(self.embedding),
|
2025-09-19 19:32:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 00:08:09 +08:00
|
|
|
|
# --- 全局单例模式 ---
|
|
|
|
|
|
|
|
|
|
|
|
_config_instance: Optional[UnifiedConfig] = None
|
2025-09-19 19:32:42 +01:00
|
|
|
|
|
|
|
|
|
|
def get_config() -> UnifiedConfig:
|
2026-02-11 00:08:09 +08:00
|
|
|
|
"""
|
|
|
|
|
|
获取全局统一配置实例。
|
|
|
|
|
|
第一次调用时,它会创建并加载配置。后续调用将返回缓存的实例。
|
|
|
|
|
|
"""
|
2025-09-19 19:32:42 +01:00
|
|
|
|
global _config_instance
|
|
|
|
|
|
if _config_instance is None:
|
|
|
|
|
|
_config_instance = UnifiedConfig()
|
|
|
|
|
|
return _config_instance
|
|
|
|
|
|
|
2026-02-11 00:08:09 +08:00
|
|
|
|
def reload_config() -> UnifiedConfig:
|
|
|
|
|
|
"""强制重新加载配置"""
|
2025-09-19 19:32:42 +01:00
|
|
|
|
global _config_instance
|
2026-02-11 00:38:23 +08:00
|
|
|
|
# 强制重新读取 .env 文件并覆盖当前环境变量
|
|
|
|
|
|
load_dotenv(override=True)
|
2025-09-19 19:32:42 +01:00
|
|
|
|
_config_instance = None
|
|
|
|
|
|
return get_config()
|