2026-02-26 13:43:44 +08:00
|
|
|
import os
|
|
|
|
|
import configparser
|
|
|
|
|
from typing import Optional
|
2026-03-24 18:07:22 +08:00
|
|
|
from pathlib import Path
|
2026-02-26 13:43:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Config:
|
|
|
|
|
"""从 config.ini 读取的应用配置"""
|
|
|
|
|
|
|
|
|
|
_config = configparser.ConfigParser()
|
2026-03-24 18:07:22 +08:00
|
|
|
_config_loaded = False
|
|
|
|
|
DEFAULT_MODEL_SECTION = "gpt-4o"
|
|
|
|
|
MAX_RETRIES = 3
|
|
|
|
|
TIMEOUT = 30
|
|
|
|
|
CONVERSATION_MAX_HISTORY_MESSAGES = 10
|
|
|
|
|
CONVERSATION_ENABLE_MULTI_TURN = False
|
|
|
|
|
CONVERSATION_ENABLE_COMPRESSION = False
|
|
|
|
|
CONVERSATION_COMPRESSION_THRESHOLD = 8
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def _get_config_path(cls) -> Path:
|
|
|
|
|
"""获取配置文件路径(支持环境变量覆盖)"""
|
|
|
|
|
env_path = os.getenv("CONFIG_PATH")
|
|
|
|
|
if env_path:
|
|
|
|
|
return Path(env_path)
|
|
|
|
|
|
|
|
|
|
project_root = Path(__file__).parent.parent
|
|
|
|
|
return project_root / 'config' / 'config.ini'
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def _load_config(cls):
|
|
|
|
|
"""懒加载配置"""
|
|
|
|
|
if cls._config_loaded:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
config_path = cls._get_config_path()
|
|
|
|
|
|
|
|
|
|
if not config_path.exists():
|
|
|
|
|
raise FileNotFoundError(
|
|
|
|
|
f"Configuration file not found at: {config_path}. "
|
|
|
|
|
"Please copy 'config/config.ini.example' to 'config/config.ini' and fill in your details."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with open(config_path, "r", encoding="utf-8") as f:
|
|
|
|
|
cls._config.read_file(f)
|
|
|
|
|
|
|
|
|
|
cls._config_loaded = True
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def reload(cls):
|
|
|
|
|
"""重新加载配置(支持热重载)"""
|
|
|
|
|
cls._config_loaded = False
|
|
|
|
|
cls._load_config()
|
|
|
|
|
_refresh_runtime_constants()
|
|
|
|
|
|
2026-02-26 13:43:44 +08:00
|
|
|
@classmethod
|
|
|
|
|
def get_model_config(cls, section: Optional[str] = None) -> dict:
|
|
|
|
|
"""
|
|
|
|
|
获取指定模型配置段。
|
|
|
|
|
若 section 为 None,则使用默认模型配置段。
|
|
|
|
|
"""
|
|
|
|
|
if section is None:
|
|
|
|
|
section = cls.DEFAULT_MODEL_SECTION
|
|
|
|
|
|
|
|
|
|
if not cls._config.has_section(section):
|
|
|
|
|
raise ValueError(f"Model section '{section}' not found in config.ini")
|
|
|
|
|
|
|
|
|
|
config = dict(cls._config.items(section))
|
|
|
|
|
|
|
|
|
|
# 确保必需的键存在
|
|
|
|
|
if 'model_name' not in config or 'openai_api_key' not in config:
|
|
|
|
|
raise ValueError(f"Model section '{section}' must contain 'model_name' and 'openai_api_key'")
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"model": config['model_name'],
|
|
|
|
|
"api_key": config['openai_api_key'],
|
|
|
|
|
"base_url": config.get('url') or config.get('base_url')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_section(cls, section: str) -> dict:
|
|
|
|
|
"""获取指定配置段的键值对(键名会被转为小写)"""
|
|
|
|
|
if not cls._config.has_section(section):
|
|
|
|
|
return {}
|
|
|
|
|
return dict(cls._config.items(section))
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def validate_config(cls):
|
|
|
|
|
"""校验默认模型配置是否存在且有效"""
|
|
|
|
|
try:
|
|
|
|
|
default_config = cls.get_model_config()
|
|
|
|
|
if not default_config.get("api_key") or 'your_openai_api_key_here' in default_config.get("api_key"):
|
|
|
|
|
raise ValueError(f"API key for default model '{cls.DEFAULT_MODEL_SECTION}' is missing or a placeholder.")
|
|
|
|
|
except (ValueError, configparser.Error) as e:
|
|
|
|
|
raise ValueError(f"Configuration validation failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
2026-03-24 18:07:22 +08:00
|
|
|
DEFAULT_MODEL_SECTION = Config.DEFAULT_MODEL_SECTION
|
|
|
|
|
MAX_RETRIES = Config.MAX_RETRIES
|
|
|
|
|
TIMEOUT = Config.TIMEOUT
|
|
|
|
|
CONVERSATION_MAX_HISTORY_MESSAGES = Config.CONVERSATION_MAX_HISTORY_MESSAGES
|
|
|
|
|
CONVERSATION_ENABLE_MULTI_TURN = Config.CONVERSATION_ENABLE_MULTI_TURN
|
|
|
|
|
CONVERSATION_ENABLE_COMPRESSION = Config.CONVERSATION_ENABLE_COMPRESSION
|
|
|
|
|
CONVERSATION_COMPRESSION_THRESHOLD = Config.CONVERSATION_COMPRESSION_THRESHOLD
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _refresh_runtime_constants() -> None:
|
|
|
|
|
"""同步模块级常量与 Config 类属性,兼容两种访问方式。"""
|
|
|
|
|
global DEFAULT_MODEL_SECTION
|
|
|
|
|
global MAX_RETRIES
|
|
|
|
|
global TIMEOUT
|
|
|
|
|
global CONVERSATION_MAX_HISTORY_MESSAGES
|
|
|
|
|
global CONVERSATION_ENABLE_MULTI_TURN
|
|
|
|
|
global CONVERSATION_ENABLE_COMPRESSION
|
|
|
|
|
global CONVERSATION_COMPRESSION_THRESHOLD
|
|
|
|
|
|
|
|
|
|
DEFAULT_MODEL_SECTION = Config._config.get('General', 'DEFAULT_MODEL_SECTION', fallback='gpt-4o')
|
|
|
|
|
MAX_RETRIES = Config._config.getint('General', 'MAX_RETRIES', fallback=3)
|
|
|
|
|
TIMEOUT = Config._config.getint('General', 'TIMEOUT', fallback=30)
|
|
|
|
|
CONVERSATION_MAX_HISTORY_MESSAGES = Config._config.getint('conversation', 'max_history_messages', fallback=10)
|
|
|
|
|
CONVERSATION_ENABLE_MULTI_TURN = Config._config.getboolean('conversation', 'enable_multi_turn', fallback=False)
|
|
|
|
|
CONVERSATION_ENABLE_COMPRESSION = Config._config.getboolean('conversation', 'enable_memory_compression', fallback=False)
|
|
|
|
|
CONVERSATION_COMPRESSION_THRESHOLD = Config._config.getint('conversation', 'compression_threshold', fallback=8)
|
|
|
|
|
|
|
|
|
|
Config.DEFAULT_MODEL_SECTION = DEFAULT_MODEL_SECTION
|
|
|
|
|
Config.MAX_RETRIES = MAX_RETRIES
|
|
|
|
|
Config.TIMEOUT = TIMEOUT
|
|
|
|
|
Config.CONVERSATION_MAX_HISTORY_MESSAGES = CONVERSATION_MAX_HISTORY_MESSAGES
|
|
|
|
|
Config.CONVERSATION_ENABLE_MULTI_TURN = CONVERSATION_ENABLE_MULTI_TURN
|
|
|
|
|
Config.CONVERSATION_ENABLE_COMPRESSION = CONVERSATION_ENABLE_COMPRESSION
|
|
|
|
|
Config.CONVERSATION_COMPRESSION_THRESHOLD = CONVERSATION_COMPRESSION_THRESHOLD
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 在类定义完成后加载配置
|
|
|
|
|
Config._load_config()
|
|
|
|
|
_refresh_runtime_constants()
|
|
|
|
|
|
|
|
|
|
|
2026-02-26 13:43:44 +08:00
|
|
|
# 如有需要可在导入时做初始校验,
|
|
|
|
|
# 但已移到 main.py 以便更可控地执行。
|
|
|
|
|
# 如需在导入时校验,可在此调用 Config.validate_config()
|